Inicjalizacja zmiennej nieznanego typu za pomocą przeciążonych konstruktorów w C ++

22

pochodzące głównie z pythonowego środowiska, z którym miałem problemy z pracą z typami w C ++.

Próbuję zainicjować zmienną klasy za pomocą jednego z kilku przeciążonych konstruktorów, które przyjmują różne typy jako parametry. Przeczytałem, że użycie autosłowa kluczowego może być użyte do automatycznego zadeklarowania zmiennej, jednak w moim przypadku nie zostanie ono zainicjowane, dopóki nie zostanie wybrany konstruktor. Jednak kompilator nie jest zadowolony z braku inicjowania 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;
    }
};

W Pythonie może to wyglądać następująco:

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

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

Jaki jest właściwy sposób użycia autosłowa kluczowego w tym scenariuszu? Czy powinienem zastosować zupełnie inne podejście?

Tomek
źródło
2
Uważam, że w ogóle nie możesz użyć autodla członków klasy? Istotne, ale nieaktualne pytanie: czy możliwe jest posiadanie zmiennej „auto” członka?
Yksisarvinen
Czy jest jakiś powód, aby nie używać szablonów?
Jimmy RT
W Pythonie typy są określane dla każdej operacji w czasie wykonywania - co wymaga narzutu, ale pozwala na zmianę typów zmiennych z jednej instrukcji na drugą. W C ++ typy muszą być znane z wyprzedzeniem, aby kod mógł się kompilować - float i int mają różne układy binarne i wymagają różnych instrukcji asemblera do pracy. Jeśli chcesz mieć elastyczność w środowisku wykonawczym, musisz użyć typu unii, takiego jak wariant, który wybiera jedną z wielu gałęzi zawierających poprawny kod dla każdego typu, co zwiększa obciążenie wydajności. Jeśli chcesz oddzielić wersje int i float, szablony są twoim przyjacielem.
Jake,

Odpowiedzi:

17

Inicjalizacja zmiennej nieznanego typu za pomocą przeciążonych konstruktorów w C ++

W C ++ nie ma czegoś takiego jak „zmienna nieznanego typu”.

Jaki jest właściwy sposób użycia słowa kluczowego auto w tym scenariuszu?

zmienne automatycznie wywnioskowane mają typ wywodzący się z inicjalizatora. Jeśli nie ma inicjalizatora, nie można użyć trybu automatycznego. Auto nie może być użyte dla niestatycznej zmiennej składowej. Jedna instancja klasy nie może mieć elementów o innym typie niż inna instancja.

W tym scenariuszu nie ma możliwości użycia słowa kluczowego auto.

Czy powinienem zastosować zupełnie inne podejście?

Prawdopodobnie. Wygląda na to, że próbujesz wdrożyć std::variant. Jeśli potrzebujesz zmiennej do przechowywania jednego z X typów, powinieneś tego użyć.

Być może próbujesz emulować dynamiczne pisanie w C ++. Chociaż może być ci znane z doświadczenia w Pythonie, w wielu przypadkach nie jest to idealne podejście. Na przykład w tym konkretnym programie przykładowym wszystko, co robisz ze zmienną składową, to wydrukuj ją. Dlatego łatwiej byłoby przechowywać ciąg w każdym przypadku. Inne podejścia to statyczny polimorfizm, jak pokazano za pomocą dynamicznego polimorfizmu w stylu Rhathin lub OOP, jak pokazano przez Fire Lancer.

eerorika
źródło
Czy użycie związku byłoby również kwalifikowalne w tym przypadku?
wondra
unionjest podatnym na błędy mechanizmem niskiego poziomu. variantprawdopodobnie używa go wewnętrznie i czyni korzystanie z niego bezpieczniejszym.
Erlkoenig,
Związek @wondra sam w sobie nie byłby bardzo przydatny, ponieważ nie można go sprawdzić, który członek jest obecnie aktywny. Bardzo bolesne jest także używanie z nietrywialnymi klasami (które mają niestandardowe destruktory), takie jak std :: string. To, czego byś chciał, to oznaczony związek. Która jest strukturą danych, którą implementuje std :: variant.
eerorika,
1
libstdc ++ 's variant robi wykorzystania union. Alternatywy, wykorzystującej surową pamięć i nowe umieszczanie, nie można użyć w constexprkonstruktorze.
Erlkoenig,
@ Erlkoenig wystarczy, cofam to, co powiedziałem. Patrzyłem tylko na wdrożenie doładowań, które nie korzystały ze związku, i założyłem, że wszyscy zrobili to samo.
eerorika
11

C ++ jest językiem o typie statycznym , co oznacza, że ​​wszystkie typy zmiennych są określane przed uruchomieniem. Dlatego autosłowo kluczowe nie jest varsłowem kluczowym w javascript, który jest językiem dynamicznie wpisywanym. autosłowo kluczowe jest powszechnie używane do określania typów, które są niepotrzebnie złożone.

To, czego szukasz, można zrobić za pomocą klasy szablonów C ++, co pozwala na tworzenie wielu wersji klasy, która przyjmuje różne typy.

Ten kod może być odpowiedzią, której szukasz.

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

Ten kod skompiluje się, jeśli zostaną spełnione pewne warunki, takie jak funkcja operator<<powinna być zdefiniowana dla std :: ostream & i wpisz T.

KimHajun
źródło
6

Innym podejściem, niż proponowali inni, jest używanie szablonów. Oto przykład:

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

Następnie możesz użyć swojej klasy w ten sposób:

Token<int> x(5);
x.printValue();
Rhathin
źródło
3

Możesz użyć tego std::varianttypu. Poniższy kod pokazuje jeden sposób (ale muszę przyznać, że jest trochę niezdarny):

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

Byłoby znacznie ładniej, gdyby std::get<0>(value)można było napisać jako, std::get<value.index()>(value)ale, niestety, „x” <x>musi być wyrażeniem stałym w czasie kompilacji.

Adrian Mole
źródło
1
Prawdopodobnie lepiej użyć std::visitzamiast switch.
eerorika
1

auto musi być możliwe do zdefiniowania dla określonego typu, nie zapewnia dynamicznego pisania w środowisku wykonawczym.

Jeśli w momencie deklarowania Tokenznasz wszystkie możliwe typy, których możesz użyć std::variant<Type1, Type2, Type3>itp. Jest to podobne do posiadania „typu enum” i „unii”. Zapewnia to wywołanie odpowiednich konstruktorów i destruktorów.

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

Alternatywą może być utworzenie innego Tokenpodtypu dla każdego przypadku (ewentualnie przy użyciu szablonów) za pomocą odpowiednich metod wirtualnych.

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;
    }
}
Lansjer ognia
źródło
0

Poniższe rozwiązanie jest podobne pod względem duchowym do rozwiązania w Fire Lancer. Kluczową różnicą jest to, że podąża za komentarzem, prawdopodobnie używając szablonów , i tym samym eliminuje potrzebę jawnego tworzenia pochodnych instancji interfejsu. Tokensama w sobie nie jest klasą interfejsu. Zamiast tego definiuje interfejs jako klasę wewnętrzną i wewnętrzną klasę szablonów do automatyzacji definicji klas pochodnych.

Jego definicja wydaje się zbyt skomplikowana. Jednak Token::Basedefiniuje interfejs i Token::Impl<>wywodzi się z interfejsu. Te wewnętrzne klasy są całkowicie ukryte dla użytkownika Token. Użycie wyglądałoby następująco:

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

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

Ponadto poniższe rozwiązanie ilustruje sposób implementacji operatora konwersji w celu przypisania Tokeninstancji do zmiennej regularnej. Opiera się na nim dynamic_casti zgłasza wyjątek, jeśli rzutowanie jest nieprawidłowe.

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

Definicja Tokenjest poniżej.

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

Wypróbuj online!

jxh
źródło