Initialisation d'une variable de type inconnu via des constructeurs surchargés en C ++

22

venant d'un arrière-plan principalement python, j'ai eu un peu de mal à travailler avec des types en C ++.

J'essaie d'initialiser une variable de classe via l'un des constructeurs surchargés qui prennent différents types comme paramètres. J'ai lu que l'utilisation du automot - clé peut être utilisée pour la déclaration automatique d'une variable, mais dans mon cas, elle ne sera pas initialisée tant qu'un constructeur n'aura pas été choisi. Cependant, le compilateur n'est pas content de ne pas s'initialiser 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, cela pourrait ressembler à:

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

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

Quelle est la bonne façon d'utiliser le automot - clé dans ce scénario? Dois-je utiliser une approche complètement différente?

À M
la source
2
Je crois que vous ne pouvez pas utiliser du tout autopour les membres de la classe? Question pertinente mais obsolète: est-il possible d'avoir une variable membre «auto»?
Yksisarvinen
Une raison de ne pas utiliser de modèles?
Jimmy RT
Avec python, les types sont déterminés pour chaque opération au moment de l'exécution - cela nécessite une surcharge, mais permet aux types de variables de passer d'une instruction à la suivante. En C ++, les types doivent être connus à l'avance, afin que le code puisse compiler - float et int ont des dispositions binaires différentes et nécessitent des instructions d'assemblage différentes pour fonctionner. Si vous voulez de la flexibilité au moment de l'exécution, vous devez utiliser un type d'union tel que variant qui choisit l'une des nombreuses branches contenant du code valide pour chaque type, en ajoutant une surcharge de performances. Si vous souhaitez conserver les versions int et float séparées, les modèles sont votre ami.
Jake

Réponses:

17

Initialisation d'une variable de type inconnu via des constructeurs surchargés en C ++

Il n'existe pas de "variable de type inconnu" en C ++.

Quelle est la bonne façon d'utiliser le mot clé auto dans ce scénario?

Les variables déduites automatiquement ont un type qui est déduit de l'initialiseur. S'il n'y a pas d'initialiseur, vous ne pouvez pas utiliser auto. auto ne peut pas être utilisé pour une variable membre non statique. Une instance d'une classe ne peut pas avoir des membres de type différent d'une autre instance.

Il n'y a aucun moyen d'utiliser le mot-clé auto dans ce scénario.

Dois-je utiliser une approche complètement différente?

Probablement. Il semble que vous essayiez d'implémenter un std::variant. Si vous avez besoin d'une variable pour stocker l'un des X types de types, c'est ce que vous devez utiliser.

Cependant, vous essayez peut-être d'émuler la saisie dynamique en C ++. Bien que cela vous soit familier en raison de votre expérience avec Python, dans de nombreux cas, ce n'est pas l'approche idéale. Par exemple, dans cet exemple de programme particulier, tout ce que vous faites avec la variable membre est de l'imprimer. Il serait donc plus simple de stocker une chaîne dans chaque cas. D'autres approches sont le polymorphisme statique tel que montré par Rhathin ou le polymorphisme dynamique de style OOP comme montré par Fire Lancer.

eerorika
la source
Le recours à l'union serait-il également admissible dans ce cas?
Wondra
unionest un mécanisme de bas niveau sujet aux erreurs. variantl'utilise probablement en interne et rend son utilisation plus sûre.
Erlkoenig
L'union @wondra en elle-même ne serait pas très utile car elle ne peut pas être inspectée pour savoir quel membre est actuellement actif. Il est également très pénible à utiliser avec des classes non triviales (qui ont un destructeur personnalisé) telles que std :: string. Ce que l'on voudrait, c'est une union étiquetée . Quelle est la structure de données que std :: variant implémente.
eerorika
1
libstdc ++ est - variant t utilisation union. L'alternative, utilisant la mémoire brute et le placement new, ne peut pas être utilisée dans un constexprconstructeur.
Erlkoenig
@Erlkoenig assez juste, je reprends ce que j'ai dit. Je n'avais regardé que la mise en œuvre des boosts, qui n'utilisait pas l'union, et je supposais que tout le monde faisait de même.
eerorika
11

C ++ est un langage typé statiquement , ce qui signifie que tous les types de variables sont déterminés avant l'exécution. Par conséquent, automot-clé n'est pas quelque chose comme varmot-clé en javascript, qui est un langage typé dynamiquement. autoLe mot-clé est couramment utilisé pour spécifier des types qui sont inutilement complexes.

Ce que vous recherchez peut être fait à l'aide de la classe de modèle C ++ à la place, ce qui permet de créer plusieurs versions de la classe qui prennent différents types.

Ce code pourrait être la réponse que vous cherchez.

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

Ce code se compilerait si certaines conditions sont remplies, comme la fonction operator<<devrait être définie pour std :: ostream & et type T.

KimHajun
la source
6

Une approche différente de celle que d'autres ont proposée consiste à utiliser des modèles. Voici un exemple:

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

Ensuite, vous pouvez utiliser votre classe comme ceci:

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

Vous pouvez utiliser le std::varianttype. Le code ci-dessous montre une façon (mais c'est un peu maladroit, je dois admettre):

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

Il serait bien plus agréable d' std::get<0>(value)écrire le std::get<value.index()>(value)mais, hélas, le "x" <x>doit être une expression constante au moment de la compilation.

Adrian Mole
la source
1
Il vaut probablement mieux utiliser à la std::visitplace de switch.
eerorika
1

auto doit être déductible à un type spécifique, il ne fournit pas de typage dynamique à l'exécution.

Si au moment de la déclaration, Tokenvous connaissez tous les types possibles que vous pouvez utiliser, std::variant<Type1, Type2, Type3>etc. Cela revient à avoir un "type enum" et une "union". Il s'assure que les constructeurs et destructeurs appropriés sont appelés.

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

Une alternative pourrait être de créer un Tokensous-type différent pour chaque cas (en utilisant éventuellement des modèles) avec des méthodes virtuelles appropriées.

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 feu
la source
0

La solution ci-dessous est similaire dans son esprit à celle de la réponse de Fire Lancer. Sa principale différence est qu'il suit le commentaire en utilisant éventuellement des modèles , et supprime ainsi la nécessité de créer explicitement des instances dérivées de l'interface. Tokenn'est pas lui-même la classe d'interface. Au lieu de cela, il définit l'interface comme une classe interne et une classe de modèle interne pour automatiser la définition des classes dérivées.

Sa définition semble trop complexe. Cependant, Token::Basedéfinit l'interface et Token::Impl<>dérive de l'interface. Ces classes internes sont entièrement cachées à l'utilisateur de Token. L'utilisation ressemblerait à:

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

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

De plus, la solution ci-dessous illustre comment on pourrait implémenter un opérateur de conversion pour affecter une Tokeninstance à une variable régulière. Il s'appuie sur dynamic_cast, et lèvera une exception si le cast n'est pas valide.

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

La définition de Tokenest ci-dessous.

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

Essayez-le en ligne!

jxh
la source