C++11奇怪的花括号初始化行为

22

我不明白C++11中大括号初始化规则在这里是如何工作的。 有以下代码:

struct Position_pod {
    int x,y,z;
};

class Position {
public:
    Position(int x=0, int y=0, int z=0):x(x),y(y),z(z){}
    int x,y,z;
};

struct text_descriptor {
    int             id;
    Position_pod    pos;
    const int       &constNum;
};

struct text_descriptor td[3] = {
     {0, {465,223}, 123},
     {1, {465,262}, 123},
};

int main() 
{
    return 0;
}

请注意,该数组被声明为拥有3个元素,但仅提供了2个初始值。

然而,它在编译时没有错误,这听起来很奇怪,因为最后一个数组元素的引用成员将未初始化。实际上,它具有NULL值:

(gdb) p td[2].constNum 
$2 = (const int &) @0x0: <error reading variable>

现在来看看“魔法”:我将Position_pod更改为Position

struct text_descriptor {
    int             id;
    Position_pod    pos;
    const int       &constNum;
};

变成这个样子:

struct text_descriptor {
    int             id;
    Position        pos;
    const int       &constNum;
};

现在它会给出预期的错误:

error: uninitialized const member ‘text_descriptor::constNum'
我的问题是:为什么在第一种情况下编译通过,而在第二种情况下应该会出现错误。 区别在于,Position_pod 使用了 C 风格的花括号初始化方式,而 Position 使用了 C++11 的初始化方式,这会调用 Position 的构造函数。但这如何影响未初始化引用成员的可能性?
(更新) 编译器: gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

9
编译器版本很重要。Clang 3.5和GCC 4.9.2无法编译此代码。 - user3920237
1
MSVC 2013 也无法编译这个。 - Ivan Aksamentov - Drop
4
不过,GCC 4.9.2 可以通过 -std=c++03 编译它。 - user743382
2
@PavelOganesyan:在那个类中有一个默认构造函数。 - Mike Seymour
2
请找一位语言律师。 - Ivan Aksamentov - Drop
显示剩余4条评论
2个回答

23

很明显

struct text_descriptor td[3] = {
     {0, {465,223}, 123},
     {1, {465,262}, 123},
};
这是列表初始化,并且初始化器列表不为空。
C++11 规定 ([dcl.init.list]p3):
列表初始化一个类型为 T 的对象或引用的定义如下:
  • 如果初始化器列表没有元素并且 T 是具有默认构造函数的类类型,则该对象将进行值初始化。
  • 否则,如果 T 是聚合体,则执行聚合初始化(8.5.1)。
  • ...

[dcl.init.aggr]p1:

聚合体是一个数组或类(第9条)没有用户提供的构造函数(12.1),非静态数据成员没有花括号或等号初始化器(9.2),没有私有或保护的非静态数据成员(第11条),没有基类(第10条),以及没有虚函数(10.3)。

td是一个数组,因此它是一个聚合体,因此执行聚合初始化。

[dcl.init.aggr]p7:

如果在初始化列表中的成员少于聚合体中的成员,则未明确初始化的每个成员必须从空初始化列表(8.5.4)初始化。这里就是这种情况,所以td[2]从一个空的初始化器列表初始化,这意味着它是值初始化。(再次参见[dcl.init.list]p3)。
而值初始化又意味着([dcl.init]p7):
对于类型为T的对象进行值初始化意味着:
如果T是具有用户提供构造函数的(可能带有cv限定符的)类类型(第9条),...
如果T是没有用户提供构造函数的(可能带有cv限定符的)非共用体类类型,则该对象将被零初始化,并且如果T的隐式声明默认构造函数是非平凡的,则调用该构造函数。
...
您的类text_descriptor是一个没有用户提供构造函数的类,因此td[2]首先被零初始化,然后调用它的构造函数。
零初始化意味着([dcl.init]p5):
要将类型为T的对象或引用“零初始化”意味着: - 如果T是标量类型(3.9),... - 如果T是(可能带有cv限定符的)非联合类类型,则对于每个非静态数据成员和每个基类子对象进行零初始化,并将填充初始化为零位; - 如果T是(可能带有cv限定符的)联合类型,则... - 如果T是数组类型,则... - 如果T是引用类型,则不执行任何初始化。
这是明确定义的,无论text_descriptor的默认构造函数如何:它只是将非引用成员和子成员进行零初始化。
然后调用默认构造函数,如果它是非平凡的。这里是默认构造函数的定义([special]p5):
一个类X的默认构造函数是一个可以不用参数调用的类X的构造函数。如果类X没有用户声明的构造函数,则将隐式声明一个没有参数的构造函数为默认值(8.4)。隐式声明的默认构造函数是其类的内联公共成员。如果类X的默认构造函数被定义为删除,则定义为删除:任何没有花括号或等号初始化器的非静态数据成员为引用类型,... 如果它不是由用户提供并且符合以下条件,则默认构造函数是平凡的:其类没有虚函数(10.3)和虚基类(10.1),其类的所有直接基类都有平凡的默认构造函数,并且对于其类的所有非静态数据成员(或数组)中的类类型,每个这样的类都有一个平凡的默认构造函数。否则,默认构造函数是非平凡的。
因此,预期的隐式定义构造函数被删除了,但如果pos是POD类型,则它也是平凡的。由于构造函数是平凡的,所以不会被调用。因为构造函数没有被调用,所以它被删除的事实并不是一个问题。
这是C++11中的一个巨大漏洞,后来已经得到修复。修复主要是为了解决无法访问的平凡默认构造函数,但修复的措辞也适用于已删除的平凡默认构造函数。N4140(大致相当于C++14)在[dcl.init.aggr]p7中指出(重点在于“我的”):如果T是一个(可能是cv限定的)类类型,没有用户提供或删除的默认构造函数,则对象被零初始化,并检查默认初始化的语义约束,如果T有一个非平凡的默认构造函数,则对象被默认初始化;
如评论中 T.C. 指出的那样,另一个DR 也作出了更改,使得 td[2] 仍然从空初始化器列表初始化,但是这个空初始化器列表现在意味着聚合初始化。这反过来又意味着 td[2] 的每个成员都会从一个空初始化器列表初始化(再次参见 [dcl.init.aggr]p7),因此似乎会使用 {} 初始化引用成员。

[dcl.init.aggr]p9 随后指出(正如 remyabel 在一篇已被删除的答案中所指出的):

如果不完整或空的 初始化器列表 未对引用类型的成员进行初始化,则程序是不合法的。

我不确定这是否适用于从隐式 {} 初始化的引用,但编译器确实将其解释为这种情况,并且没有其他可能的解释。


引用无法进行值初始化。 - user3920237
@remyabel 我知道,但是类的值初始化并不意味着该类成员的值初始化。 - user743382
@bogdan 它确实有一个默认构造函数。该默认构造函数已被删除,因此如果使用该默认构造函数,则会出现错误,但由于措辞上的差异,它甚至没有被使用。我引用了标准中的[special]p5,其中指定了何时定义默认构造函数以及如何定义它。 - user743382
2
C++14还交换了[dcl.init.list]p2的前两个项目,因此无论初始化程序列表是否为空,它始终是聚合初始化。 - T.C.
@T.C. 我在编辑中解决了这个问题,但我不确定该程序是否在C++14中有效。你有什么想法吗? - user743382
显示剩余2条评论

-1

第一个版本(带有_pod后缀的版本)仍然可用,但不会报错,因为对于z值,选择了int的默认值(0)。const int引用也是如此。

在第二个版本中,您无法定义一个没有给定值的const引用。编译器会报错,因为之后不能给它赋任何值。

此外,你使用的编译器在这里扮演了重要的角色,可能是一个bug,只是因为你在声明一个类成员前声明了一个int成员。


2
但是两个版本都试图在没有给它赋值的情况下初始化引用。问题是关于数组的初始化器数量,而不是每个数组元素的“Position”成员。 (回答您的问题:演示编译器的行为如何随着将该成员的类型从POD更改为非POD而发生变化。) - Mike Seymour
@MikeSeymour 这是一个编译器问题。而且你在结构体中还包含了一个类。如果你保持结构简单,或者在第一个结构中使用构造函数会更好。 - madduci
1
“这似乎是编译器的问题。”但是你的回答并没有说那个,而且似乎在说第一个版本是正确的,尽管它实际上不是。 - Mike Seymour
可能是一个排序错误,因为你在 int 之前定义了一个类成员。 - madduci

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