使用非平凡构造函数初始化联合体

65

我有一个结构体,我创建了一个自定义构造函数将其成员初始化为 0。在旧的编译器中,如果不对值进行memset操作,处于发布模式时这些值就没有被初始化。

现在我想在联合中使用这个结构体,但由于它具有非平凡构造函数,所以会出错。

问题1:默认编译器实现的构造函数是否保证所有结构体成员都将被空初始化?非平凡构造函数只是对所有成员执行memset操作以确保结构干净。

问题2:如果必须在基础结构上指定构造函数,如何实现联合以包含该元素并确保基本元素被初始化为 0?

7个回答

54

问题1:根据C++标准, 默认构造函数会将POD成员初始化为0。请参见下面引用的文本。

问题2:如果一个基类必须指定构造函数,则该类不能是联合体的一部分。

最后,您可以为您的联合提供一个构造函数:

union U 
{
   A a;
   B b;

   U() { memset( this, 0, sizeof( U ) ); }
};

针对问题1:

来自C++03标准,12.1构造函数,第190页

隐式定义的默认构造函数执行了该类的初始化集合,这些初始化将由一个用户编写的空的mem-initializer-list(12.6.2)和空函数体的默认构造函数执行。

来自C++03标准,8.5初始化器,第145页

默认初始化类型T的对象意味着:

  • 如果T是非POD类类型(第9条款),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化是不合法的);
  • 如果T是数组类型,则每个元素都进行默认初始化;
  • 否则,对象被零初始化

零初始化类型T的对象意味着:

  • 如果T是标量类型(3.9),则将对象设置为0(零)转换为T的值;
  • 如果T是非联合类类型,则每个非静态数据成员和每个基类子对象都进行零初始化
  • 如果T是联合类型,则对象的第一个命名数据成员进行零初始化;
  • 如果T是数组类型,则每个元素都进行零初始化;
  • 如果T是引用类型,则不执行任何初始化。

针对问题2:

来自C++03标准,12.1构造函数,第190页

如果一个构造函数是隐式声明的默认构造函数,并且:

  • 它的类没有虚函数(10.3)和虚基类(10.1),并且
  • 其类的所有直接基类都有平凡的构造函数,并且
  • 对于其类的所有非静态数据成员,如果其类型为类类型(或其数组),则每个这样的类都有平凡的构造函数

来自C++03标准,9.5联合体,第162页

联合体可以有成员函数(包括构造函数和析构函数),但不能有虚函数(10.3)。联合体不得具有基类。不能将联合体用作基类。带有非平凡构造函数(12.1)、非平凡复制构造函数(12.8)、非平凡析构函数(12.4)或非平凡复制赋值运算符(13.5.3、12.8)的类的对象不能是联合体的成员,也不能是这些对象的数组。


23
尽管有“默认构造函数”这个名称,但缺失的是默认构造函数不会对POD成员进行默认初始化。根据您从12.1中引用的语句,12.6.2/4说明了当成员没有在初始值列表中提到时会发生什么情况,这适用于隐式构造函数。它说,“如果实体是......类类型的非静态数据成员,且实体类是非POD类,则实体被默认初始化......否则,该实体不会被初始化”。因此,POD数据成员不会通过隐式生成的构造函数进行初始化。非POD数据成员会进行默认初始化。 - Steve Jessop
4
这个答案已经过时了,请查看我下面关于C++11的答案。 - dan-man

43

C++11带来了更好的变化。

Stroustrup亲自描述的,现在你可以合法地进行下面的操作(我从C++11 Wikipedia文章上找到了这个链接)。

维基百科上给出的示例代码如下:

#include <new> // Required for placement 'new'.

struct Point {
    Point() {}
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};

union U {
    int z;
    double w;
    Point p; // Illegal in C++03; legal in C++11.
    U() {new(&p) Point();} // Due to the Point member, a constructor
                           // definition is now *required*.
};

Stroustrup提供了更详细的说明。


1
从技术上讲,你的建议需要一个构造函数是不正确的。与任何其他成员一样,只有在使用时才需要它。原始存储可以初始化为int、double或Point,然后可以使用指向U的指针来访问它(接着是适当的字段名)。示例用途包括解释对象流、模拟堆栈或仅解释在堆上分配的值。 - Yttrill
1
在您提供的示例中,与维基百科不同,无需使用placement new。通过为U提供一个用户定义的构造函数,例如:U() {},它的成员p将被默认初始化为不确定的值。显然,这并不使p成为活动成员,因此从其任何成员读取都将是UB,有多种原因。成员p也可以在不使用placement new的情况下激活,因为它的复制构造函数是平凡的,如:U u; u.p = {1, 2}; - 303
1
@303: 这似乎是有效的:U() : p() {}GodBolt)。 - einpoklum
1
@einpoklum 那个构造函数似乎允许类似于以下用例:U u; u.p.x_ = 3; 不过,我不确定这是否符合您想要分享的示例代码。无论如何,回顾我的先前评论,由于部分不正确,我很快就会将其删除。我可能期望赋值表达式 U u; u.p = {1, 2}; 被视为一种复制列表初始化形式,并在重载决议期间考虑 p 的平凡复制构造函数。 - 303
1
经过仔细查看,代码 U u; u.p = {1, 2}; 实际上调用了 p 的自动生成的复制赋值运算符,而不是任何构造函数。由于赋值操作只能在已存在的对象上执行,因此行为是未定义的。需要使用放置 new 来启动非平凡的非活动联合成员的生命周期,例如:U u; new (&u.p) Point{1, 2}; - 303
显示剩余18条评论

3
据我所知,联合成员可能没有构造函数或析构函数。
问题1:不,没有这样的保证。任何未在构造函数初始化列表中的POD成员都会被默认初始化,但是这是使用您定义的构造函数并具有初始化程序列表的情况下。如果您不定义构造函数,或者您定义一个没有初始化程序列表和空主体的构造函数,则POD成员将不会被初始化。
非POD成员始终将通过其默认构造函数构造,如果合成,则再次不会初始化POD成员。鉴于联合成员可能没有构造函数,您几乎可以保证联合中结构体的POD成员不会被初始化。
问题2:您始终可以像这样初始化结构/联合:
struct foo
{
    int a;
    int b;
};

union bar
{
    int a;
    foo f;
};

bar b = { 0 };

3
你可以给 union 本身一个构造函数,将自己 memset 为零。 - Greg Rogers
好观点!我自己也经常忘记联合构造函数! - unwesen
1
程序员默认构造函数没有初始化列表和空体的情况和编译器生成的构造函数之间没有区别。 - David Rodríguez - dribeas
@dribeas:谢谢,我写得不是很清楚,已经更新了我的答案。 - unwesen
1
一个区别是前者使类成为非POD,而后者则不会。 - Steve Jessop

3
正如格雷格·罗杰斯在unwesen的帖子中提到的那样,您可以为您的联合体提供一个构造函数(如果您愿意还可以提供一个析构函数):
struct foo
{
    int a;
    int b;
};

union bar
{
    bar() { memset(this, 0, sizeof(*this)); }

    int a;
    foo f;
};

1
看起来我需要一些教育。使用memset将对象清零,会抹掉类的虚表吗? - EvilTeach
6
两件事情,1)你不必使用虚函数表来实现多态(尽管所有人都这样做)。 2)你在foo上看到任何虚方法吗?或者说根本没有任何方法吗?它继承自任何东西吗?如果没有虚方法,就没有虚函数表。实际上,如果foo有虚方法并且由此具有虚函数表,那么它将不再是POD,因此不符合union的成员资格。 - Logan Capaldo

0

你能做类似这样的事情吗?

class Outer
{
public:
    Outer()
    {
        memset(&inner_, 0, sizeof(inner_));
    }
private:
    union Inner
    {
        int qty_;
        double price_;
    } inner_;
};

...或者类似这样的东西?

union MyUnion
{
    int qty_;
    double price_;
};

void someFunction()
{
    MyUnion u = {0};
}

我们曾经考虑过这个问题,但是我们试图放入联合体的结构在代码的其他部分中已经被使用了,因此删除构造函数(假设编译器将结构体视为POD并且不会将所有元素初始化为0)可能会破坏依赖于它的代码。 - Superpolock

0

这是一个有趣的问题,其他答案中有很多有用的信息。此外,了解通过=default语法指定默认构造函数的效果将非常有用。

对于作为联合成员的类,这样的“默认”默认构造函数优于没有初始化列表和空主体的用户定义的默认构造函数。注意:如果用户定义的默认构造函数是非平凡的,例如调用memset等,则dan-man的答案显示需要做什么(即使e.g.将默认构造函数定义为没有初始化列表和空主体)。

关于问题1,“默认”默认构造函数将突出显示默认初始化和值初始化之间的区别。

对于一个名为C的类,如果默认构造函数被用户显式定义为C() {}(即,具有空体和无初始化列表),那么当通过这种方式创建对象时,它将导致默认初始化:C c_obj{};。然而,如果默认构造函数被指定为C()=default;,那么C c_obj{};将导致c_obj的值初始化。

关于问题2,dan-man的答案非常有用。使用“defaulted”默认构造函数可以简化如下:

#include <new> // Required for placement 'new'.

struct Point {
    Point()=default; // not `Point() {};`
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};

union U {
    int z;
    double w;
    Point p;
    // No need to specify a default constructor.
    // It is needed with `Point() {};` which is considered 
    // as a user defined default constructor.
};

int main() {
...
U u; // implicitly generated default constructor of U is called.

new(&u.p)Point(); // activate the Point member of U
                  // using placement new.
...
}

-3

您需要等待编译器支持C++0x才能获得这个功能。在此之前,请见谅。


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