Ініціалізація змінної невідомого типу через перевантажені конструктори в C ++


22

Виходячи з фона в основному пітона, я дещо боровся з роботою з типами в 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" члена?
Іксісарвінен

Будь-яка причина не використовувати шаблони?
Jimmy RT

З використанням python типи визначаються для кожної операції під час виконання - що вимагає накладних витрат, але дозволяє змінним типам змінюватися від одного оператора до іншого. У C ++ типи потрібно знати заздалегідь, щоб код міг компілювати - float та int мають різні бінарні макети та потребують різних інструкцій зі складання. Якщо ви хочете гнучкості під час виконання, вам потрібно використовувати тип об'єднання, такий як варіант, який вибирає одну з багатьох гілок, що містять дійсний код для кожного типу, додаючи накладні витрати. Якщо ви хочете тримати версію int та float окремо, шаблони є вашим другом.
Джейк

Відповіді:


17

Ініціалізація змінної невідомого типу через перевантажені конструктори в C ++

У C ++ немає такого поняття, як "змінна невідомого типу".

Який правильний спосіб використання ключового слова авто в цьому сценарії?

автоматично виведені змінні мають тип, який виводиться з ініціалізатора. Якщо немає ініціалізатора, ви не можете використовувати авто. auto не можна використовувати для нестатичної змінної члена. Один екземпляр класу не може мати інші типи членів, ніж інший.

У цьому сценарії використання автоматичного ключового слова немає.

Чи варто взагалі використовувати інший підхід?

Мабуть. Схоже, ви намагаєтесь реалізувати std::variant. Якщо вам потрібна змінна для зберігання одного з X числа типів, саме цим ви повинні скористатися.

Однак, можливо, ви намагаєтесь наслідувати динамічне введення тексту на C ++. Хоча це може бути вам знайоме завдяки досвіду роботи з Python, у багатьох випадках це не ідеальний підхід. Наприклад, у цій конкретній прикладній програмі все, що ви робите зі змінною члена, - це надрукувати її. Тож було б простіше зберігати рядок у кожному випадку. Інші підходи - це статичний поліморфізм, як показано в динамічному поліморфізмі стилю Rhathin або OOP, як показав Fire Lancer.


Чи вдалося б також використовувати об'єднання в цьому випадку?
wondra

union- механізм низького рівня схильності до помилок. variantймовірно, використовує його внутрішньо і робить його використання більш безпечним.
Erlkoenig

Самс @wondra не був би дуже корисним, оскільки його неможливо перевірити, для якого учасника зараз активний. Також дуже болісно використовувати з нетривіальними класами (у яких є власний деструктор), наприклад std :: string. Чого б хотілося, - це тегований союз. Яка структура даних, яку реалізує std :: variant.
eerorika

1
libstdc ++ 's variant робить використання union. Альтернативу, використовуючи необмежену пам'ять та нове розміщення, не можна використовувати в constexprконструкторі.
Erlkoenig

@Erlkoenig досить чесно, я повертаю те, що я сказав. Я лише дивився на прискорене впровадження, яке не використовувало об'єднання, і припускав, що всі роблять те саме.
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.


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

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

Спробуйте в Інтернеті!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.