Inicializando uma variável de tipo desconhecido por meio de construtores sobrecarregados em C ++

22

vindo de um background principalmente python, eu tenho um pouco de dificuldade em trabalhar com tipos em C ++.

Estou tentando inicializar uma variável de classe por meio de um dos vários construtores sobrecarregados que usam tipos diferentes como parâmetros. Eu li que o uso da autopalavra - chave pode ser usado para declaração automática de uma variável, no entanto, no meu caso, não será inicializado até que um construtor seja escolhido. No entanto, o compilador não está satisfeito por não inicializar value.

class Token {
public:

    auto value;

    Token(int ivalue) {
        value = ivalue;
    }
    Token(float fvalue) {
        value = fvalue;
    }
    Token(std::string svalue) {
        value = svalue;
    }

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

Em python, isso pode se parecer com:

class Token():
        def __init__(self, value):
             self.value = value

        def printValue(self):
             print("The token value is: %s" % self.value)

Qual é a maneira correta de usar a autopalavra - chave nesse cenário? Devo usar uma abordagem completamente diferente?

Tom
fonte
2
Eu acredito que você não pode usar autopara os alunos? Pergunta relevante, mas desatualizada: É possível ter uma variável de membro “auto”?
Yksisarvinen 18/12/19
Algum motivo para não usar modelos?
Jimmy RT
Com o python, os tipos são determinados em cada operação em tempo de execução - isso requer sobrecarga, mas permite que os tipos de variáveis ​​sejam alterados de uma instrução para a seguinte. No C ++, os tipos precisam ser conhecidos antecipadamente, para que o código possa compilar - flutuar e int ter layouts binários diferentes e exigir instruções de montagem diferentes para trabalhar. Se você deseja flexibilidade no tempo de execução, precisa usar um tipo de união, como a variante, que escolha uma das muitas ramificações que contêm código válido para cada tipo, adicionando sobrecarga de desempenho. Se você deseja manter as versões int e float separadas, os modelos são seus amigos.
Jake

Respostas:

17

Inicializando uma variável de tipo desconhecido por meio de construtores sobrecarregados em C ++

Não existe "variável de tipo desconhecido" em C ++.

Qual é a maneira correta de usar a palavra-chave automática nesse cenário?

variáveis ​​deduzidas automaticamente têm um tipo deduzido do inicializador. Se não houver inicializador, não será possível usar automático. auto não pode ser usado para uma variável de membro não estático. Uma instância de uma classe não pode ter membros de tipo diferente de outra instância.

Não há como usar a palavra-chave automática neste cenário.

Devo usar uma abordagem diferente completamente?

Provavelmente. Parece que você está tentando implementar um std::variant. Se você precisar de uma variável para armazenar um número X de tipos, é isso que você deve usar.

No entanto, você pode estar tentando emular a digitação dinâmica em C ++. Embora possa ser familiar para você devido à experiência com o Python, em muitos casos essa não é a abordagem ideal. Por exemplo, neste programa de exemplo específico, tudo o que você faz com a variável membro é imprimi-lo. Portanto, seria mais simples armazenar uma string em cada caso. Outras abordagens são o polimorfismo estático, como mostrado pelo polimorfismo dinâmico no estilo Rhathin ou OOP, como mostrado pelo Fire Lancer.

eerorika
fonte
Usar união também se qualificaria neste caso?
Wondra
unioné um mecanismo de baixo nível propenso a erros. variantprovavelmente o usa internamente e torna seu uso mais seguro.
Erlkoenig 19/12/19
A união @wondra por si só não seria muito útil, pois não pode ser inspecionada para saber qual membro está ativo no momento. Também é muito doloroso usar com classes não triviais (que possuem destruidor personalizado), como std :: string. O que se deseja é uma união marcada . Qual é a estrutura de dados que std :: variant implementa.
eerorika
11
libstdc ++ 's variant faz uso union. A alternativa, usando memória bruta e posicionamento novo, não pode ser usada em um constexprconstrutor.
Erlkoenig 19/12/19
@ Erlkoenig justo o suficiente, retiro o que disse. Eu tinha olhado apenas para impulsionar a implementação, que não usava union, e supus que todos fizessem o mesmo.
eerorika
11

C ++ é uma linguagem de tipo estaticamente , o que significa que todos os tipos de variáveis ​​são determinados antes do tempo de execução. Portanto, autopalavra-chave não é algo como varpalavra-chave em javascript, que é uma linguagem de tipo dinâmico. autoA palavra-chave é comumente usada para especificar tipos desnecessariamente complexos.

O que você está procurando pode ser feito usando a classe de modelo C ++, o que permite criar várias versões da classe que usa tipos diferentes.

Esse código pode ser a resposta que você está procurando.

template <typename T>
class Token {
private:
    T value;

public:
    Token(const T& ivalue) {
        value = ivalue;
    }

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

Esse código seria compilado se algumas condições fossem atendidas, como a função operator<<deve ser definida para std :: ostream & e tipo T.

KimHajun
fonte
6

Uma abordagem diferente da que outros propuseram é usar modelos. Aqui está um exemplo:

template<class T>
class Token {
public:

    T value;

    Token(T value) :
        value(std::move(value))
    {}

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

Então você pode usar sua classe assim:

Token<int> x(5);
x.printValue();
Rhathin
fonte
3

Você pode usar o std::varianttipo O código abaixo mostra uma maneira (mas é um pouco desajeitado, devo admitir):

#include <iostream>
#include <variant>

class Token {
public:

    std::variant<int, float, std::string> value;

    Token(int ivalue) {
        value = ivalue;
    }
    Token(float fvalue) {
        value = fvalue;
    }
    Token(std::string svalue) {
        value = svalue;
    }

    void printValue() {
        switch (value.index()) {
            case 0:
                std::cout << "The token value is: " << std::get<0>(value) << std::endl;
                break;
            case 1:
                std::cout << "The token value is: " << std::get<1>(value) << std::endl;
                break;
            case 2:
                std::cout << "The token value is: " << std::get<2>(value) << std::endl;
                break;
        }
    }
};

int main() {
    Token it(1);
    Token ft(2.2f);
    Token st("three");
    it.printValue();
    ft.printValue();
    st.printValue();
    return 0;
}

Seria muito melhor se o std::get<0>(value)poderia ser escrito como std::get<value.index()>(value), mas, infelizmente, o "x" in <x>deve ser uma expressão constante em tempo de compilação.

Adrian Mole
fonte
11
Provavelmente melhor usar em std::visitvez de switch.
eerorika
1

auto deve ser dedutível para um tipo específico, não fornece digitação dinâmica em tempo de execução.

Se, no momento da declaração, Tokenvocê souber todos os tipos possíveis, poderá usar std::variant<Type1, Type2, Type3>etc. Isso é semelhante a ter um "tipo enum" e uma "união". Isso garante que os construtores e destruidores adequados sejam chamados.

std::variant<int, std::string> v;
v = "example";
v.index(); // 1, a int would be 0
std::holds_alternative<std::string>(v); // true
std::holds_alternative<int>(v); // false
std::get<std::string>(v); // "example"
std::get<int>(v); // throws std::bad_variant_access

Uma alternativa poderia ser criar um Tokensubtipo diferente para cada caso (possivelmente usando modelos) com métodos virtuais adequados.

class Token {
public:
    virtual void printValue()=0;
};

class IntToken : public Token {
public:
    int value;
    IntToken(int ivalue) {
        value = ivalue;
    }
    virtual void printValue()override
    {
        std::cout << "The token value is: " << value << std::endl;
    }
}
Fire Lancer
fonte
0

A solução abaixo é semelhante em espírito à da resposta de Fire Lancer. A principal diferença é que segue o comentário possivelmente usando modelos e, assim, elimina a necessidade de criar instâncias derivadas explicitamente da interface. Tokennão é ela mesma a classe de interface. Em vez disso, define a interface como uma classe interna e uma classe de modelo interna para automatizar a definição das classes derivadas.

Sua definição parece excessivamente complexa. No entanto, Token::Basedefine a interface e Token::Impl<>deriva da interface. Essas classes internas estão totalmente ocultas para o usuário de Token. O uso seria semelhante a:

Token s = std::string("hello");
Token i = 7;

std::cout << "The token value is: " << s << '\n';
std::cout << "The token value is: " << i << '\n';

Além disso, a solução abaixo ilustra como alguém pode implementar um operador de conversão para atribuir uma Tokeninstância a uma variável regular. Depende dynamic_caste lançará uma exceção se o elenco for inválido.

int j = i; // Allowed
int k = s; // Throws std::bad_cast

A definição de Tokenestá abaixo.

class Token {

    struct Base {
        virtual ~Base () = default;
        virtual std::ostream & output (std::ostream &os) = 0;
    };

    template <typename T>
    struct Impl : Base {
        T val_;
        Impl (T v) : val_(v) {}
        operator T () { return val_; }
        std::ostream & output (std::ostream &os) { return os << val_; }
    };

    mutable std::unique_ptr<Base> impl_;

public:

    template <typename T>
    Token (T v) : impl_(std::make_unique<Impl<T>>(v)) {}

    template <typename T>
    operator T () const { return dynamic_cast<Impl<T>&>(*impl_); }

    friend auto & operator << (std::ostream &os, const Token &t) {
        return t.impl_->output(os);
    }
};

Experimente online!

jxh
fonte