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

Posted

技术标签:

【中文标题】通过 C++ 中的重载构造函数初始化未知类型的变量【英文标题】:Initialising a variable of unknown type via overloaded constructors in C++ 【发布时间】:2020-04-11 02:03:10 【问题描述】:

主要来自 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 关键字的正确方法是什么?我应该完全使用不同的方法吗?

【问题讨论】:

我相信你根本就不能用auto 给班级成员?相关但过时的问题:Is it possible to have an “auto” member variable? 有什么理由不使用模板? 使用 python,类型是在运行时在每个操作上确定的——这需要开销,但允许变量类型从一个语句更改为下一个语句。在 C++ 中,需要提前知道类型,以便代码可以编译 - float 和 int 具有不同的二进制布局,并且需要不同的汇编指令才能使用。如果您希望在运行时具有灵活性,则需要使用联合类型(例如变体)选择包含每种类型的有效代码的多个分支之一,从而增加性能开销。如果您想将 int 和 float 版本分开,模板是您的朋友。 【参考方案1】:

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

C++ 中不存在“未知类型的变量”。

在这种情况下使用 auto 关键字的正确方法是什么?

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

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

我应该完全使用不同的方法吗?

大概吧。看起来您正在尝试实现std::variant。如果您需要一个变量来存储 X 种类型中的一种,那么您应该使用它。

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

【讨论】:

union 是一种容易出错的低级机制。 variant 可能在内部使用它并使其使用更安全。 @wondra union 本身并不是很有用,因为它无法检查哪个成员当前处于活动状态。与 std::string 等非平凡类(具有自定义析构函数)一起使用也非常痛苦。人们想要的是一个 tagged 联合。这是 std::variant 实现的数据结构。 libstdc++ 的 variant does 使用 union。使用原始内存和放置 new 的替代方法不能在 constexpr 构造函数中使用。 @Erlkoenig 很公平,我收回我所说的话。我只查看了没有使用联合的 boosts 实现,并假设每个人都这样做。【参考方案2】:

C++ 是statically typed language,这意味着所有变量类型都是在运行前确定的。因此,auto 关键字不像 javascript 中的 var 关键字,它是一种动态类型语言。 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;
    
;

如果满足某些条件,则此代码将编译,例如应该为 std::ostream& 和类型 T 定义函数operator&lt;&lt;

【讨论】:

【参考方案3】:

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

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

【讨论】:

【参考方案4】:

您可以使用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&lt;0&gt;(value) 可以写成std::get&lt;value.index()&gt;(value) 会更好,但是,唉,&lt;x&gt; 中的“x”必须是编译时常量表达式。

【讨论】:

最好使用std::visit 而不是switch【参考方案5】:

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

如果在声明Token 时您知道所有可能的类型,您可以使用std::variant&lt;Type1, Type2, Type3&gt; 等。这类似于拥有“类型枚举”和“联合”。它确保调用正确的构造函数和析构函数。

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;
    

【讨论】:

【参考方案6】:

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

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

【讨论】:

以上是关于通过 C++ 中的重载构造函数初始化未知类型的变量的主要内容,如果未能解决你的问题,请参考以下文章

C++中的复制构造函数

C++类和对象下

C++类-构造函数的重载

C++类-构造函数的重载

11.6 C++构造函数重载

11.6 C++构造函数重载