【问题标题】:C++ primer 5th edition. Union and members of class typeC++ 入门第 5 版。工会和班级类型的成员
【发布时间】:2021-10-22 21:48:21
【问题描述】:

我有这个来自 C++ 入门第 5 版的文本。 ch 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;
}

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

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

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

sval = s;

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

【问题讨论】:

  • 想象一下当联合持有其他东西时调用字符串析构函数,这可能在指向字符串字节的堆分配部分的字节中。
  • 记住sval = ssval.operator=(s) 是一样的,因为sval 还没有被构造,这不会起作用,因为同样的原因你不能调用任何成员函数在sval.
  • @John Kugelman:如何像编辑过的那样突出显示文本引用中的代码?
  • “高亮代码”是什么意思?您是在问我如何将代码放入块引用中?我通过在代码前面加上> 加上五个空格来做到这一点。 "> " 用于块引用," " 用于代码格式化。
  • @JohnKugelman:我的意思是,就像你在编辑我的代码时所做的那样。例如,如果我想从包含我的主题中的代码的书中复制文本。我从 ">" 行开始,但不能显示像你这样的代码。

标签: c++ union


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

    我认为没有必要明确调用 dtor sval.~string() 因为赋值运算符会这样做。

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

    【讨论】:

    • 首先感谢您提供如此有用的解释。第二:如果我没记错的话:union 的问题是它拥有分配的未初始化内存,所以我需要在尝试分配之前在那里构造一个std::string 类型的对象?正确的?由于这个事实,这对我来说是一种误导,例如:struct Foo{int x_; std::string str_; char c_}; 这里每当构造一个 Foo 类型的对象,成员 x_c_ 和一个字符串对象 (str_) 是通过其定义构造的ctor (std::string()) 与 union 不同,其中成员即使是堆栈内存也不会自动构造。
    • 1.对,2。不,联盟的所有三个成员都像这样同时构建在逻辑上是不可能的。毕竟,工会的目的是所有工会成员的记忆是共同的,是共享的。如果一个联合的所有三个成员都被构造出来,就像struct,这会导致三个不同的对象存在于同一内存中。完成这一壮举需要违反我们共享宇宙的几项基本物理规则。在这种情况下,三个成员都没有正式建造。
    • 我说它们(联合的成员)不是自动构造的,当然编译器不知道哪个是活动成员,因此调用它的构造函数,这就是为什么它们没有在 ctor 中初始化-init 列表。
    猜你喜欢
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    • 2021-08-25
    • 2021-11-24
    • 1970-01-01
    • 1970-01-01
    • 2021-07-14
    • 2021-07-28
    相关资源
    最近更新 更多