Inicializar una variable de tipo desconocido a través de constructores sobrecargados en C ++

22

viniendo de un fondo principalmente de Python, he tenido dificultades para trabajar con tipos en C ++.

Estoy intentando inicializar una variable de clase a través de uno de varios constructores sobrecargados que toman diferentes tipos como parámetros. He leído que usar la autopalabra clave se puede usar para la declaración automática de una variable, sin embargo, en mi caso, no se inicializará hasta que se elija un constructor. Sin embargo, el compilador no está contento con no 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;
    }
};

En python esto podría verse así:

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

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

¿Cuál es la forma correcta de usar la autopalabra clave en este escenario? ¿Debo usar un enfoque completamente diferente?

Tom
fuente
2
¿Creo que no puedes usarlo autopara los miembros de la clase? Pregunta relevante pero desactualizada: ¿Es posible tener una variable miembro "auto"?
Yksisarvinen
¿Alguna razón para no usar plantillas?
Jimmy RT
Con python, los tipos se determinan en cada operación en tiempo de ejecución, lo que requiere una sobrecarga, pero permite que los tipos de variables cambien de una declaración a la siguiente. En C ++, los tipos deben conocerse de antemano, de modo que el código pueda compilarse: float e int tienen diferentes diseños binarios y requieren diferentes instrucciones de ensamblaje para trabajar. Si desea flexibilidad en el tiempo de ejecución, debe usar un tipo de unión, como una variante, que elija una de las muchas ramas que contienen código válido para cada tipo, agregando una sobrecarga de rendimiento. Si desea mantener las versiones int y float separadas, las plantillas son su amigo.
Jake

Respuestas:

17

Inicializar una variable de tipo desconocido a través de constructores sobrecargados en C ++

No existe tal cosa como "variable de tipo desconocido" en C ++.

¿Cuál es la forma correcta de usar la palabra clave automática en este escenario?

Las variables auto deducidas tienen un tipo que se deduce del inicializador. Si no hay inicializador, no puede usar auto. auto no se puede usar para una variable miembro no estática. Una instancia de una clase no puede tener miembros con un tipo diferente que otra instancia.

No hay forma de usar la palabra clave automática en este escenario.

¿Debo usar un enfoque completamente diferente?

Probablemente. Parece que estás intentando implementar a std::variant. Si necesita una variable para almacenar uno de X número de tipos, eso es lo que debe usar.

Sin embargo, puede estar tratando de emular la escritura dinámica en C ++. Si bien puede ser familiar para usted debido a la experiencia con Python, en muchos casos ese no es el enfoque ideal. Por ejemplo, en este programa de ejemplo en particular, todo lo que hace con la variable miembro es imprimirlo. Por lo tanto, sería más sencillo almacenar una cadena en cada caso. Otros enfoques son el polimorfismo estático como lo muestra Rhathin o el polimorfismo dinámico de estilo OOP como lo muestra Fire Lancer.

eerorika
fuente
¿Usar la unión también calificaría en este caso?
wondra
uniones un mecanismo de bajo nivel propenso a errores. variantprobablemente lo usa internamente y hace que su uso sea más seguro.
Erlkoenig
@wondra union por sí sola no sería muy útil ya que no se puede inspeccionar qué miembro está actualmente activo. También es muy doloroso usarlo con clases no triviales (que tienen un destructor personalizado) como std :: string. Lo que uno querría es una unión etiquetada . Cuál es la estructura de datos que implementa std :: variant.
eerorika
1
libstdc ++ 's variant hace uso union. La alternativa, usando memoria en bruto y ubicación nueva, no puede usarse en un constexprconstructor.
Erlkoenig
@Erlkoenig, bastante justo, retiro lo que dije. Solo había mirado la implementación de aumentos, que no usaba union, y asumí que todos hacían lo mismo.
eerorika
11

C ++ es un lenguaje de tipo estático , lo que significa que todos los tipos de variables se determinan antes del tiempo de ejecución. Por lo tanto, la autopalabra clave no es algo así como la varpalabra clave en javascript, que es un lenguaje de tipo dinámico. autoLa palabra clave se usa comúnmente para especificar tipos que son innecesariamente complejos.

Lo que está buscando podría hacerse utilizando la clase de plantilla C ++, que permite crear múltiples versiones de la clase que toman diferentes tipos.

Este código puede ser la respuesta que estás buscando.

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;
    }
};

Este código se compilaría si se cumplen algunas condiciones, como la función operator<<debería definirse para std :: ostream & y type T.

KimHajun
fuente
6

Un enfoque diferente, de lo que otros han propuesto, es usar plantillas. Aquí hay un ejemplo:

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;
    }
};

Entonces puedes usar tu clase así:

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

Puedes usar el std::varianttipo. El siguiente código muestra una forma (pero debo admitir que es un poco torpe):

#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;
}

Sería mucho mejor si se std::get<0>(value)pudiera escribir como std::get<value.index()>(value)pero, por desgracia, la "x" <x>debe ser una expresión constante en tiempo de compilación.

Adrian Mole
fuente
1
Probablemente mejor usar en std::visitlugar de switch.
eerorika
1

auto debe ser deducible a un tipo específico, no proporciona escritura dinámica en tiempo de ejecución.

Si al momento de declarar Tokenconoce todos los tipos posibles que puede usar, std::variant<Type1, Type2, Type3>etc. Esto es similar a tener una "enumeración de tipos" y una "unión". Se asegura de que se llame a los constructores y destructores adecuados.

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

Una alternativa podría ser crear un Tokensubtipo diferente para cada caso (posiblemente usando plantillas) con métodos virtuales adecuados.

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;
    }
}
Lancer de fuego
fuente
0

La solución a continuación es similar en espíritu a la respuesta de Fire Lancer. Su diferencia clave es que sigue el comentario posiblemente usando plantillas , y así elimina la necesidad de crear instancias derivadas creadas explícitamente de la interfaz. Tokenno es en sí la clase de interfaz. En cambio, define la interfaz como una clase interna y una clase de plantilla interna para automatizar la definición de las clases derivadas.

Su definición parece demasiado compleja. Sin embargo, Token::Basedefine la interfaz y Token::Impl<>deriva de la interfaz. Estas clases internas están completamente ocultas para el usuario de Token. El uso se vería así:

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

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

Además, la solución a continuación ilustra cómo se podría implementar un operador de conversión para asignar una Tokeninstancia a una variable regular. Se basa dynamic_casty arrojará una excepción si el lanzamiento no es válido.

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

La definición de Tokenestá abajo.

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);
    }
};

Pruébalo en línea!

jxh
fuente