C++初级读本第5版.工会和类类型的成员



我有C++入门第5版的这篇文章。ch 19.6 Union:

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

在这种情况下,如果并集已经包含一个字符串,我们可以使用普通的字符串赋值运算符为该字符串赋予一个新值。否则,就没有可调用字符串赋值运算符的现有字符串对象。相反,我们必须在内存中构造一个字符串来保存并集。我们使用placement new(§19.1.2,p.824)在sval所在的位置构造字符串。我们将该字符串初始化为字符串参数的副本。我们接下来更新判别式并返回。

对我来说,一切都很简单,但让我困惑的是这个版本的复制赋值运算符(需要std::string):以及最后一段:";如果并集已经持有字符串"但如果union不包含string,那么我们为什么要使用这样的placement new呢?只要赋值运算符调用析构函数,从而破坏对象,然后释放内存,这与构造函数相反?

我是说他为什么不直接写这个?

sval = s;

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

不能为从未构造过的对象赋值。在C++中,任何对象,任何对象都需要在发生任何事情之前先构造。这是C++的基本规则之一,没有任何例外或替代方案。

在构造对象之前,试图以任何方式使用对象都会导致未定义的行为。分配给一个对象就等于在使用它。因为,毕竟,如果你要给一个物体分配一些东西,它一定已经存在了。

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

实际上,你是在提议将某个东西分配给一个对象,而没有构建它的好处。这是未定义的行为。

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

我认为没有必要用sval~string(),因为赋值运算符会执行此操作。

否,作为一般规则:赋值运算符不调用析构函数。赋值运算符通常不会破坏对象。如果是的话,对象将不复存在,但它在被指定后会存在。

另一种看待这一问题的方法是想象当sval不包含正确构造的std::string时,如果执行sval = s会发生什么。

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

现在,当您执行sval = s时,sval的副本分配操作符将执行如下操作:

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

  2. 为新的字符数组分配空间,然后跨字符数组从s复制数据。

如果sval未正确初始化,则步骤1将失败。字符数组数据可能是一些随机指针值,所以,轰!如果手动调用sval的析构函数,同样的参数也适用。

因此使用了放置CCD_ 19。这没有任何先决条件,并且在sval中构造了一个有效的、空的std::string,然后您可以安全地将其分配给它。

最新更新