联合体成员的析构函数似乎会自动调用

3

我试图实现一个标记联合。

我的理解是,在C++联合中,非平凡(即非空)的非静态成员的析构函数永远不会被调用,因此我们必须自己调用它们。这就是我所做的:

#include <iostream>

class C {
public:
  C() {
    std::cout << "C Ctor" << std::endl;
  }
  ~C() {
    std::cout << "C Dtor" << std::endl;
  }
};

class B {
public:
  B() {
    std::cout << "B Ctor" << std::endl;
  }
  ~B() {
    std::cout << "B Dtor" << std::endl;
  }
};

struct S {
  int type;

  union U {
    C c;
    B b;

    U() {

    }

    ~U() {}
  } u;

  S(int type) : type(type) {
    if (type == 0) {
      u.c = C();
    } else {
      u.b = B();
    }
  }

  ~S() {
    if (type == 0) {
      u.c.~C();
    } else {
      u.b.~B();
    }
  }
};

int main() {
  S s(0);
  return 0;
}

然而,输出结果为:
C Ctor
C Dtor
C Dtor

这意味着,C的析构函数被调用了两次,而不是一次。

到底发生了什么?如果您注意到我的标记联合实现中存在其他问题,请指出。


FYI:通常情况下,std::endl是不必要的。 "\n"同样具有可移植性,并且不会尝试在每行刷新输出。 - HTNW
通常情况下,您可以使用匿名联合体来实现标记联合体,您可以在标准中阅读相关内容和/或查看std::variant的实现。 - M.M
2个回答

6

S(int type) : type(type) {
    if (type == 0) {
      u.c = C();
    } else {
      u.b = B();
    }
  }
  

由于您位于构造函数的主体内,因此u.c = C();不是初始化,而是赋值。这意味着您会看到为C()调用的构造函数,并且在表达式的末尾,第一个析构函数调用被调用以销毁该临时对象。我们可以通过添加来验证这一点。

C& operator=(const C&) { std::cout << "operator=(const C&)\n"; return *this; }

转换为C语言将输出更改为

C Ctor
operator=(const C&)
C Dtor
C Dtor

第二个析构函数调用发生在s超出main作用域并运行它的析构函数。


请注意,目前的代码是未定义行为。联合体不会在您编写的用户提供的构造函数中激活成员,因此当您执行以下操作时

u.c = C();

你正在给一个还没有创建的对象赋值。你不能修改一个尚未被创建的对象。


我明白了。所以析构函数会为临时变量调用一次,而成员变量只会被调用一次。是这样吗?如果是的话,您会建议如何更改呢?我在构造体中实例化C而不是在初始化列表中,原因是我需要根据“类型”进行分支...我想我可以在初始化列表中使用三元运算符,但这种做法常见吗? - Aviv Cohn
2
@AvivCohn 使用放置new像这样 S(int type) : type(type) { if (type == 0) { new (&u.c) C(); } else { new (&u.b) B(); } }。这将给你输出 C Ctor C Dtor - NathanOliver
1
@AvivCohn你的示例无法正常工作的原因是因为它具有未定义行为,因为它调用了非活动联合成员的赋值运算符,而该成员是非平凡可构造的。在评论中,Nathan的建议不会这样做,并且行为是明确定义的。 - eerorika
@eerorika 我明白了 - 我本来打算就像问题中展示的那样在堆栈上创建一个临时变量,但是你是说这实际上是非法的,因为给尚未构造的联合体赋值是未定义行为?如果我理解正确,那么在C++中唯一的方法是使用放置new或构造函数初始化程序列表来分配值给尚未构造的非平凡构造联合体成员。对吗? - Aviv Cohn
4
据我所知,是的,激活一个非平凡联合体成员的唯一方法是使用placement new。 - eerorika
显示剩余6条评论

2

在构造函数中,您创建了C类的临时实例:

u.c = C();

这是一个被复制并销毁的实例,因此输出的前两行属于此实例。而最后一行输出是你 ~S() 调用的结果。

除此之外,在 C++17 中,你还可以使用标准强大的标记联合实现:https://en.cppreference.com/w/cpp/utility/variant


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接