Инициализация переменной неизвестного типа через перегруженные конструкторы в C ++

22

Исходя из опыта работы с Python, я немного боролся с работой с типами в C ++.

Я пытаюсь инициализировать переменную класса через один из нескольких перегруженных конструкторов, которые принимают разные типы в качестве параметров. Я прочитал, что использование autoключевого слова может быть использовано для автоматического объявления переменной, однако в моем случае оно не будет инициализировано до тех пор, пока не будет выбран конструктор. Однако компилятор не рад тому, что он не инициализирован 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;
    }
};

В Python это может выглядеть так:

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

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

Как правильно использовать autoключевое слово в этом сценарии? Должен ли я использовать другой подход в целом?

Том
источник
2
Я считаю, что вы не можете использовать autoдля учеников вообще? Актуальный, но устаревший вопрос: возможно ли иметь переменную-член auto?
Иксисарвинен
Есть ли причина не использовать шаблоны?
Джимми RT
В python типы определяются для каждой операции во время выполнения, что требует дополнительных затрат, но позволяет переменным типам переходить от одного оператора к другому. В C ++ типы должны быть известны заранее, чтобы код мог компилироваться - float и int имеют разные двоичные макеты и требуют разных инструкций по сборке для работы. Если вам нужна гибкость во время выполнения, вам нужно использовать тип объединения, такой как вариант, который выбирает одну из многих ветвей, содержащих допустимый код для каждого типа, добавляя накладные расходы производительности. Если вы хотите разделить версии int и float, шаблоны - ваш друг.
Джейк

Ответы:

17

Инициализация переменной неизвестного типа через перегруженные конструкторы в C ++

В C ++ нет такой вещи как «переменная неизвестного типа».

Как правильно использовать ключевое слово auto в этом сценарии?

автоматически выводимые переменные имеют тип, который выводится из инициализатора. Если инициализатор отсутствует, вы не можете использовать авто. auto нельзя использовать для нестатической переменной-члена. Один экземпляр класса не может иметь членов с другим типом, чем другой экземпляр.

В этом сценарии нельзя использовать ключевое слово auto.

Должен ли я использовать другой подход в целом?

Вероятно. Похоже, вы пытаетесь реализовать std::variant. Если вам нужна переменная для хранения одного из X типов, это то, что вы должны использовать.

Однако вы можете пытаться эмулировать динамическую типизацию в C ++. Хотя он может быть вам знаком по опыту работы с Python, во многих случаях это не идеальный подход. Например, в этом конкретном примере программы все, что вы делаете с переменной-членом, - это печатаете ее. Так что было бы проще хранить строку в каждом случае. Другие подходы - статический полиморфизм, как показано динамическим полиморфизмом в стиле Ратина или ООП, как показано в Fire Lancer.

eerorika
источник
Будет ли использование союза также квалифицироваться в этом случае?
Wondra
unionявляется механизмом низкого уровня, подверженным ошибкам. variantвероятно, использует его внутри и делает его использование более безопасным.
Erlkoenig
@wondra union сам по себе не будет очень полезен, так как его нельзя проверить, для какого члена активен в данный момент. Также очень больно использовать с нетривиальными классами (которые имеют собственный деструктор), такими как std :: string. То, что вы хотите, это помеченный союз. Какая структура данных реализована в std :: option.
eerorika
1
libstdc ++ 's variant делает использование union. Альтернатива, использующая необработанную память и размещение новой, не может быть использована в constexprконструкторе.
Erlkoenig
@Erlkoenig достаточно справедливо, я забираю то, что я сказал. Я только посмотрел на реализацию Boots, которая не использует union, и предположил, что все делают то же самое.
eerorika
11

C ++ является статически типизированным языком , что означает, что все типы переменных определяются до времени выполнения. Следовательно, autoключевое слово не является чем-то похожим на varключевое слово в javascript, который является языком с динамической типизацией. autoКлючевое слово обычно используется для указания типов, которые излишне сложны.

Вместо этого вы можете использовать шаблонный класс C ++, который позволяет создавать несколько версий класса, которые принимают разные типы.

Этот код может быть ответом, который вы ищете.

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

Этот код компилируется, если выполняются некоторые условия, например, функция operator<<должна быть определена для std :: ostream & и типа T.

KimHajun
источник
6

Другой подход, чем предложенный другими, заключается в использовании шаблонов. Вот пример:

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

Тогда вы можете использовать свой класс следующим образом:

Token<int> x(5);
x.printValue();
Rhathin
источник
3

Вы можете использовать std::variantтип. Приведенный ниже код демонстрирует один из способов (но я должен признать, что он немного неуклюжий):

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

Было бы намного лучше, если std::get<0>(value)бы их можно было записать как, std::get<value.index()>(value)но, увы, «х» в <x>должно быть константным выражением времени компиляции.

Адриан Моул
источник
1
Вероятно, лучше использовать std::visitвместо switch.
eerorika
1

auto должен быть выводимым для определенного типа, он не обеспечивает динамическую типизацию во время выполнения.

Если во время объявления Tokenвы знаете все возможные типы, которые вы можете использовать std::variant<Type1, Type2, Type3>и т. Д. Это похоже на наличие «enum типа» и «union». Это гарантирует, что правильные конструкторы и деструкторы будут вызваны.

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

Альтернативой может быть создание другого Tokenподтипа для каждого случая (возможно, с использованием шаблонов) с помощью подходящих виртуальных методов.

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;
    }
}
Огненный Лансер
источник
0

Решение, приведенное ниже, по духу аналогично тому, что было в ответе Огненного Лансера. Его ключевое отличие состоит в том, что он следует за комментарием, возможно, с использованием шаблонов , и, таким образом, устраняет необходимость явного создания производных экземпляров интерфейса. Tokenсам по себе не интерфейсный класс. Вместо этого он определяет интерфейс как внутренний класс и внутренний шаблонный класс для автоматизации определения производных классов.

Это определение кажется слишком сложным. Однако Token::Baseопределяет интерфейс и является Token::Impl<>производным от интерфейса. Эти внутренние классы полностью скрыты от пользователя Token. Использование будет выглядеть так:

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

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

Кроме того, приведенное ниже решение иллюстрирует, как можно реализовать оператор преобразования для назначения Tokenэкземпляра обычной переменной. Он полагается dynamic_castи выдаст исключение, если приведение неверно.

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

Определение Tokenниже.

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

Попробуйте онлайн!

jxh
источник