C++ 入门第 5 版。 class 类型的联盟和成员

C++ primer 5th edition. Union and members of class type

我有这篇来自 C++ primer 第 5 版的课文。第 19.6 章联盟:

class Token {
public:
    Token(): tok(INT), ival{0} { }
    Token(const Token &t): tok(t.tok) { copyUnion(t); }
    Token &operator=(const Token&);
    ~Token() { if (tok == STR) sval.~string(); }
    Token &operator=(const std::string&);
    Token &operator=(char);
    Token &operator=(int);
    Token &operator=(double);
private:
    enum {INT, CHAR, DBL, STR} tok; // discriminant
    union {                         // anonymous union
        char   cval;
        int    ival;
        double dval;
        std::string sval;
    }; // each Token object has an unnamed member of this unnamed union type
    // check the discriminant and copy the union member as appropriate
    void copyUnion(const Token&);
};

Token &Token::operator=(const std::string &s)
{
    if (tok == STR) // if we already hold a string, just do an assignment
       sval = s;
    else
       new(&sval) string(s); // otherwise construct a string
    tok = STR;                // update the discriminant
    return *this;
}

In this case, if the union already holds a string, we can use the normal string assignment operator to give a new value to that string. Otherwise, there is no existing string object on which to invoke the string assignment operator. Instead, we must construct a string in the memory that holds the union. We do so using placement new (§ 19.1.2, p. 824) to construct a string at the location in which sval resides. We initialize that string as a copy of our string parameter. We next update the discriminant and return.

一切对我来说都很简单,但令我困惑的是这个版本的复制赋值运算符(需要一个std::string):以及最后一段:“如果联合已经包含一个字符串.. 。”但是,如果 union 不包含 string 那么我们为什么要费心使用这样的 placement new?只要赋值运算符调用析构函数从而销毁对象然后释放内存,这与构造函数相反?

我的意思是他为什么不直接写这个?

sval = s;

我认为没有必要使用 sval.~string() 显式调用 dtor,因为赋值运算符会这样做。我的意思是不需要任何条件。

您不能将某些东西分配给从未构造过的对象。在 C++ 中,任何对象,任何对象都需要先构造,然后再发生任何事情。这是 C++ 的基本规则之一,没有任何例外或替代方案。

在构造对象之前尝试以任何方式使用它会导致未定义的行为。分配给一个对象算作使用它。因为,毕竟,如果您要将某物分配给某个对象,它必须已经存在。

这是一个关键的基本概念。构造一个对象和给它赋值是有很大区别的。在第一种情况下,该对象最初并不存在。在第二种情况下它已经存在,这意味着它一定是在某个未指定的先前时间点被构建的。

实际上,您是在提议将某些内容分配给一个对象,而没有构造它的好处。这是未定义的行为。

P.S。在现代 C++ 中,您几乎可以忽略已阅读的所有内容,而只需使用 std::variant,它会为您处理所有这些细节。然而,理解这些基本概念确实很重要,这是一个很好的例子来说明它们。但是在你弄清楚为什么事情必须是这样之后,在这里,你几乎可以忘记它,只需使用 std::variant.

I think there's no need to call the dtor explicitly with sval.~string() because the assignment operator does that instead.

不,一般规则是:赋值运算符不调用析构函数。一般来说,赋值运算符不会破坏对象。如果是,该对象将不再存在,但它当然会在分配给之后存在。

另一种看待这个问题的方法是想象如果您在 sval 不包含正确构造的 std::string.

时执行 sval = s 会发生什么

假设,为了论证,std::string 对象在内部分配一个字符数组来保存存储在字符串中的实际数据(它几乎必须这样做,不管怎样)。为了简单起见,我在这里忽略了短字符串优化。

现在,当您执行 sval = s 时,sval 的复制赋值运算符将要执行的操作如下所示:

  1. 释放sval中已有的字符数组数据。

  2. 为新字符数组分配 space,然后从 s.

    复制字符数组数据

如果 sval 没有正确初始化,那么第 1 步将会失败。字符数组数据可能是一些随机指针值,所以,砰!如果您手动调用 sval 的析构函数,同样的论点也适用。

因此使用展示位置 new。这没有先决条件,并在 sval 中构造了一个有效的空 std::string,然后您可以安全地分配给它。