通过 C++ 中的重载构造函数初始化未知类型的变量

Initialising a variable of unknown type via overloaded constructors in C++

主要来自 python 背景,我在使用 C++ 类型方面遇到了一些困难。

我正在尝试通过采用不同类型作为参数的多个重载构造函数之一来初始化 class 变量。我读过使用 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 关键字的正确方法是什么?我应该完全使用不同的方法吗?

Initialising a variable of unknown type via overloaded constructors in C++

C++中没有"variable of unknown type"这样的东西。

What is the right way of using the auto keyword in this scenario?

自动推导的变量具有从初始化程序推导的类型。如果没有初始化程序,则不能使用自动。 auto 不能用于非静态成员变量。 class 的一个实例不能与另一个实例具有不同类型的成员。

在这种情况下无法使用 auto 关键字。

Should I use a different approach altogether?

可能吧。您似乎正在尝试实施 std::variant。如果您需要一个变量来存储 X 种类型中的一种,那么您应该使用它。

但是,您可能会尝试在 C++ 中模拟动态类型。由于使用 Python 的经验,您可能对它很熟悉,但在许多情况下这并不是理想的方法。例如,在这个特定的示例程序中,您对成员变量所做的一切就是打印它。所以在每种情况下存储一个字符串会更简单。其他方法是 as shown by Rhathin or OOP style ,如 Fire Lancer 所示。

auto 必须可推导出特定类型,它不提供运行时动态类型。

如果在声明 Token 时您知道所有可能的类型,您可以使用 std::variant<Type1, Type2, Type3> 等。这类似于 "type 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;
    }
}

与其他人提出的不同的方法是使用模板。这是一个例子:

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

然后你可以像这样使用你的class:

Token<int> x(5);
x.printValue();

您可以使用 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" 必须是一个编译时常量表达式。

C++是一个statically typed language,意思是所有的变量类型都是在运行前确定的。因此,auto 关键字不同于 javascript 中的 var 关键字,后者是一种动态类型语言。 auto 关键字通常用于指定不必要复杂的类型。

您正在寻找的可能会通过使用 C++ 模板 class 来完成,它允许创建采用不同类型的 class 的多个版本。

这段代码可能就是您正在寻找的答案。

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。

下面的解决方案在本质上与 Fire Lancer 的回答中的解决方案相似。它的主要区别在于它遵循注释 可能使用模板,因此无需显式创建接口的派生实例。 Token 本身不是接口 class。相反,它将接口定义为内部 class 和内部模板 class 以自动定义派生的 classes.

它的定义显得过于复杂。但是,Token::Base定义接口,Token::Impl<>派生自接口。这些内部 class 对 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);
    }
};

Try it online!