Initialisieren einer Variablen unbekannten Typs über überladene Konstruktoren in C ++

22

Ich komme hauptsächlich aus Python und habe Probleme mit der Arbeit mit Typen in C ++.

Ich versuche, eine Klassenvariable über einen von mehreren überladenen Konstruktoren zu initialisieren, die verschiedene Typen als Parameter verwenden. Ich habe gelesen, dass die Verwendung des autoSchlüsselworts für die automatische Deklaration einer Variablen verwendet werden kann. In meinem Fall wird sie jedoch erst initialisiert, wenn ein Konstruktor ausgewählt wird. Der Compiler ist jedoch nicht glücklich darüber, dass er nicht initialisiert wird 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;
    }
};

In Python könnte dies so aussehen:

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

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

Wie wird das autoSchlüsselwort in diesem Szenario richtig verwendet ? Sollte ich einen anderen Ansatz wählen?

Tom
quelle
2
Ich glaube, Sie können überhaupt nicht autofür Klassenmitglieder verwenden? Relevante, aber veraltete Frage: Ist es möglich, eine "automatische" Mitgliedsvariable zu haben?
Yksisarvinen
Gibt es einen Grund, keine Vorlagen zu verwenden?
Jimmy RT
Mit Python werden Typen für jede Operation zur Laufzeit festgelegt. Dies erfordert Overhead, ermöglicht jedoch den Wechsel von Variablentypen von einer Anweisung zur nächsten. In C ++ müssen die Typen im Voraus bekannt sein, damit Code kompiliert werden kann - float und int haben unterschiedliche binäre Layouts und erfordern unterschiedliche Montageanweisungen, um damit arbeiten zu können. Wenn Sie zur Laufzeit Flexibilität wünschen, müssen Sie einen Vereinigungstyp wie eine Variante verwenden, der einen von vielen Zweigen mit gültigem Code für jeden Typ auswählt, wodurch der Leistungsaufwand erhöht wird. Wenn Sie die Versionen int und float getrennt halten möchten, sind Vorlagen Ihr Freund.
Jake

Antworten:

17

Initialisieren einer Variablen unbekannten Typs über überladene Konstruktoren in C ++

In C ++ gibt es keine "Variable unbekannten Typs".

Wie kann das Schlüsselwort auto in diesem Szenario richtig verwendet werden?

Automatisch abgeleitete Variablen haben einen Typ, der vom Initialisierer abgeleitet wird. Wenn kein Initialisierer vorhanden ist, können Sie auto nicht verwenden. auto kann nicht für eine nicht statische Elementvariable verwendet werden. Eine Instanz einer Klasse kann keine anders typisierten Mitglieder haben als eine andere Instanz.

In diesem Szenario gibt es keine Möglichkeit, das Schlüsselwort auto zu verwenden.

Sollte ich einen anderen Ansatz wählen?

Wahrscheinlich. Es sieht so aus, als würden Sie versuchen, a zu implementieren std::variant. Wenn Sie eine Variable zum Speichern eines von X Typen benötigen, sollten Sie diese verwenden.

Möglicherweise versuchen Sie jedoch, die dynamische Eingabe in C ++ zu emulieren. Obwohl es Ihnen aufgrund der Erfahrung mit Python vielleicht vertraut ist, ist dies in vielen Fällen nicht der ideale Ansatz. In diesem speziellen Beispielprogramm müssen Sie beispielsweise nur die Mitgliedsvariable drucken. Es wäre also einfacher, jeweils einen String zu speichern. Andere Ansätze sind statischer Polymorphismus, wie er von Rhathin gezeigt wird, oder dynamischer Polymorphismus im OOP-Stil, wie er von Fire Lancer gezeigt wird.

Eerorika
quelle
Würde sich die Verwendung von Union auch in diesem Fall qualifizieren?
Wondra
unionist ein fehleranfälliger Low-Level-Mechanismus. variantverwendet es wahrscheinlich intern und macht seine Verwendung sicherer.
Erlkoenig
@wondra union allein wäre nicht sehr nützlich, da nicht überprüft werden kann, welches Mitglied gerade aktiv ist. Es ist auch sehr schmerzhaft, mit nicht trivialen Klassen (die einen benutzerdefinierten Destruktor haben) wie std :: string zu verwenden. Was man sich wünschen würde, ist eine getaggte Gewerkschaft. Welches ist die Datenstruktur, die std :: variante implementiert.
Eerorika
1
libstdc ++ 's variant tut Gebrauch union. Die Alternative, bei der Rohspeicher und Platzierung neu verwendet werden, kann in einem constexprKonstruktor nicht verwendet werden .
Erlkoenig
@Erlkoenig fair genug, ich nehme zurück, was ich gesagt habe. Ich hatte mir nur die Implementierung von Boosts angesehen, bei denen keine Gewerkschaft verwendet wurde, und war davon ausgegangen, dass alle das Gleiche taten.
Eerorika
11

C ++ ist eine statisch typisierte Sprache , dh alle Variablentypen werden vor der Laufzeit festgelegt. Daher ist das autoSchlüsselwort kein varSchlüsselwort in Javascript, einer dynamisch typisierten Sprache. autoDas Schlüsselwort wird häufig verwendet, um Typen anzugeben, die unnötig komplex sind.

Was Sie suchen, können Sie stattdessen mit der C ++ - Vorlagenklasse tun, mit der Sie mehrere Versionen der Klasse erstellen können, die unterschiedliche Typen annehmen.

Dieser Code könnte die Antwort sein, nach der Sie suchen.

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

Dieser Code wird kompiliert, wenn einige Bedingungen erfüllt sind, z. B. sollte die Funktion operator<<für std :: ostream & und Typ T definiert werden.

KimHajun
quelle
6

Ein anderer Ansatz als der, den andere vorgeschlagen haben, ist die Verwendung von Vorlagen. Hier ist ein Beispiel:

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

Dann können Sie Ihre Klasse folgendermaßen verwenden:

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

Sie können den std::variantTyp verwenden. Der folgende Code zeigt einen Weg (aber es ist ein bisschen ungeschickt, muss ich zugeben):

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

Es wäre viel schöner, wenn das std::get<0>(value)so geschrieben werden könnte, std::get<value.index()>(value)aber leider muss das "x" in <x>ein konstanter Ausdruck zur Kompilierungszeit sein.

Adrian Mole
quelle
1
Wahrscheinlich besser zu verwenden std::visitals switch.
Eerorika
1

auto muss auf einen bestimmten Typ ableitbar sein, bietet keine dynamische Typisierung zur Laufzeit.

Wenn Sie zum Zeitpunkt der Deklaration Tokenalle möglichen Typen kennen, die Sie verwenden können std::variant<Type1, Type2, Type3>usw. Dies ähnelt einer "Typaufzählung" und einer "Vereinigung". Es stellt sicher, dass die richtigen Konstruktoren und Destruktoren aufgerufen werden.

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

Eine Alternative könnte darin bestehen, Tokenfür jeden Fall einen anderen Subtyp (möglicherweise unter Verwendung von Vorlagen) mit geeigneten virtuellen Methoden zu erstellen .

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
quelle
0

Die folgende Lösung ähnelt im Geiste der Antwort von Fire Lancer. Der Hauptunterschied besteht darin, dass der Kommentar möglicherweise mithilfe von Vorlagen verfolgt wird und somit keine explizit erstellten abgeleiteten Instanzen der Schnittstelle mehr erforderlich sind. Tokenist nicht selbst die Schnittstellenklasse. Stattdessen wird die Schnittstelle als innere Klasse und als innere Vorlagenklasse definiert, um die Definition der abgeleiteten Klassen zu automatisieren.

Die Definition erscheint zu komplex. Allerdings Token::Basedefiniert die Schnittstelle und Token::Impl<>leitet sich von der Grenzfläche. Diese inneren Klassen sind dem Benutzer von vollständig verborgen Token. Die Verwendung würde wie folgt aussehen:

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

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

Die folgende Lösung zeigt auch, wie ein Konvertierungsoperator implementiert werden kann, um Tokeneiner regulären Variablen eine Instanz zuzuweisen . Es stützt sich auf dynamic_castund löst eine Ausnahme aus, wenn die Besetzung ungültig ist.

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

Die Definition von Tokenist unten.

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

Probieren Sie es online aus!

jxh
quelle