为什么委托给默认构造函数不会对成员变量进行零初始化?

7
#include <string>

struct T1 {
  int _mem1;
  int _mem2;
  T1() = default;
  T1(int mem2) : T1() { _mem2 = mem2; }
};
T1 getT1() { return T1(); }
T1 getT1(int mem2) { return T1(mem2); }
int main() {
  volatile T1 a = T1();
  std::printf("a._mem1=%d a._mem2=%d\n", a._mem1, a._mem2);
  volatile T1 b = T1(1);
  std::printf("b._mem1=%d b._mem2=%d\n", b._mem1, b._mem2);
  // Temporarily disable
  if (false) {
    volatile T1 c = getT1();
    std::printf("c._mem1=%d c._mem2=%d\n", c._mem1, c._mem2);
    volatile T1 d = getT1(1);
    std::printf("d._mem1=%d d._mem2=%d\n", d._mem1, d._mem2);
  }
}

当我使用gcc5.4编译时,得到以下输出:
g++ -std=c++11 -O3 test.cpp -o test && ./test
a._mem1=0 a._mem2=0
b._mem1=382685824 b._mem2=1

为什么委托给默认构造函数的用户定义构造函数无法将b的_mem1设置为零,而使用默认构造函数的a会初始化为零?
Valgrind也证实了这一点:
==12579== Conditional jump or move depends on uninitialised value(s)
==12579==    at 0x4E87CE2: vfprintf (vfprintf.c:1631)
==12579==    by 0x4E8F898: printf (printf.c:33)
==12579==    by 0x4005F3: main (in test)

如果我把"if(false)"改成"if(true)",那么输出结果就和你预期的一样。
a._mem1=0 a._mem2=0
b._mem1=0 b._mem2=1
c._mem1=0 c._mem2=0
d._mem1=0 d._mem2=1

编译器是做什么的?
2个回答

2

简短回答: 对于平凡类型,"默认构造"的两种不同形式会导致两种不同的初始化:

  • T a; 的情况下,对象是默认初始化的。它的值是未确定的,未定义行为很快就会发生(这就是b.mem1的初始化方式,以及valgrind检测到错误的原因)。
  • T a=T(); 的情况下,对象是值初始化的,它的整个内存都被清零了(这就是a.mem1a.mem2发生的情况)。

详细回答: 实际上,T1的默认构造函数并不是导致a.mem1被零初始化的原因。a首先被零初始化,但b没有被零初始化,因为标准有一个奇特的规则,不适用于b的初始化器。

定义volatile a=T()会导致a值初始化 (1)struct T1没有用户提供的默认构造函数(2)。对于这样的结构体,整个对象都被零初始化,正如C++11标准的这条规则所述[dcl.init]/7.2:

如果T是一个(可能带有cv限定符的)非联合类类型,并且没有用户提供的构造函数,则对象将被零初始化,如果T的隐式声明的默认构造函数是非平凡的,则调用该构造函数。


在C++11和C++17之间存在微妙的差别,导致定义volatile b=T(1)在C++11中是未定义行为,但在C++17中不是。在C++11中,b通过复制一个对象类型T1进行初始化,该对象类型由表达式T(1)初始化。这个复制构造函数评估了T(1).mem1,这是一个未确定的值。这是禁止的。在C++17中,b直接由prvalue表达式T(1)初始化。
printf内部评估此未确定的值也是未定义行为,与c++标准无关。这就是为什么valgrind会抱怨,并且当您将if(true)更改为if(false)时,为什么会看到不一致的输出。 (1) 严格来说,在C++11中a是从一个值初始化的对象进行复制构造的 (2) T1的默认构造函数不是用户提供的,因为它在第一次声明时被定义为默认

0

简短回答

您代码中的default构造函数被认为是平凡的,这种构造函数不执行任何操作,即使事物未初始化。

更长的回答

Trivial default constructor

The default constructor for class T is trivial (i.e. performs no action) if all of the following is true:

The constructor is not user-provided (i.e., is implicitly-defined or defaulted on its first declaration)
T has no virtual member functions
T has no virtual base classes 

T has no non-static members with default initializers. 

(since C++11)

Every direct base of T has a trivial default constructor
Every non-static member of class type has a trivial default constructor 

> A trivial default constructor is a constructor that performs no action. All data types compatible with the C language (POD types) are trivially default-constructible. Unlike in C, however, objects with trivial default constructors cannot be created by simply reinterpreting suitably aligned storage, such as memory allocated with std::malloc: placement-new is required to formally introduce a new object and avoid potential undefined behavior.

http://www.enseignement.polytechnique.fr/informatique/INF478/docs/Cpp/en/cpp/language/default_constructor.html


如果默认构造函数什么也不做,为什么'a'会被初始化为零呢? - jonynz
@jonynz 当调用默认构造函数 A a() 时,它被称为值初始化,在调用默认构造函数之前进行零初始化。如果您调用 A a; 它将不会进行零初始化(与委托构造函数相同)。 - code707
@code707,你是在指这个吗:“如果T是一个类类型,并且它有一个既不是用户提供的也不是已删除的默认构造函数(也就是说,它可能是一个具有隐式定义或默认定义的默认构造函数的类),则对象将被零初始化,然后如果它有一个非平凡的默认构造函数,则进行默认初始化;”,作为https://en.cppreference.com/w/cpp/language/value_initialization的一部分? 那么对于b,以下内容“如果T是一个具有任何类型的至少一个用户提供的构造函数的类类型,则调用默认构造函数;”是正确的吗? - jonynz
是的,完全正确。基本上这意味着当您处理构造时,编译器不会进行零初始化,但当编译器处理时,它会执行。 - code707
这个答案太简单了。并非每个default构造函数都是平凡的。特别地,当其中一个成员具有非平凡的默认构造函数时,默认构造函数就不是平凡的。(但在给定的示例中,没有这样的成员,因此T1::T1是平凡的) - MSalters
抱歉耽搁了,这个答案仍然没有解释为什么:volatile T1 a = T1(); 被初始化为零,但不是 volatile T1 b = T1(1); - jonynz

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