默认值和零初始化混乱


88

我对value-&default-&zero-initialization感到非常困惑。特别是当他们采用不同的标准C ++ 03C ++ 11(和C ++ 14)时。

我在这里引用并尝试扩展一个很好的答案Value- / Default- / Zero-Init C ++ 98C ++ 03,使其更通用,因为如果有人可以帮助填写,它将对很多用户有所帮助需要差距才能很好地了解何时会发生什么?

简而言之,通过示例全面了解:

有时由new运算符返回的内存将被初始化,有时并不取决于您要更新的类型是POD(普通旧数据),还是它是包含POD成员并且正在使用a的类。编译器生成的默认构造函数。

  • C ++ 1998中,有两种初始化类型:初始化和默认初始化
  • C ++ 2003中,第三种初始化类型是值初始化
  • C ++ 2011 / C ++ 2014中,仅添加了列表初始化,并且value- / default- / zero-initialization的规则有所更改。

承担:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

在C ++ 98编译器中,应该发生以下情况

  • new A -不确定值(A是POD)
  • new A()-零初始化
  • new B -默认构造(B::m未初始化,B非POD)
  • new B()-默认构造(B::m未初始化)
  • new C -默认构造(C::m零初始化,C非POD)
  • new C()-默认构造(C::m初始化为零)
  • new D -默认构造(D::m未初始化,D非POD)
  • new D()-默认构造?D::m未初始化)

在符合C ++ 03的编译器中,事情应该像这样运行:

  • new A -不确定值(A是POD)
  • new A() -值初始化 A,因为它是一个POD,所以为零初始化。
  • new B -默认初始化(B::m未初始化,B是非POD)
  • new B() -值初始化 B,将所有字段归零,因为它的默认ctor是由编译器生成的,而不是用户定义的。
  • new CC-default -initializes ,它调用默认的ctor。(C::m为零初始化,C为非POD)
  • new C()C-value -initializes ,它调用默认的ctor。(C::m零初始化)
  • new D -默认构造(D::m未初始化,D非POD)
  • new D() -值是否初始化D?,它会调用默认的ctor(D::m未初始化)

斜体值和?有不确定性,请帮助纠正此问题:-)

在符合C ++ 11的编译器中,事情应该像这样运行:

??? (如果我从这里开始,请帮忙,否则还是会出错)

在C ++ 14符合的编译器,事情应该像这样: ??? (如果我从这里开始,请务必提供帮助,否则会出错) (根据答案起草)

  • new A -default-initializes A,编译器生成。ctor,(A::m未初始化的叶子)(A是POD)

  • new A() -值初始化A,这是因为在2点的零初始化[dcl.init] / 8

  • new B -default-initializes B,编译器生成。ctor,(B::m未初始化的叶子)(B是非POD)

  • new B() -值初始化B,将所有字段归零,因为它的默认ctor是由编译器生成的,而不是用户定义的。

  • new CC-default -initializes ,它调用默认的ctor。(C::m为零初始化,C为非POD)

  • new C()C-value -initializes ,它调用默认的ctor。(C::m零初始化)

  • new D -default-initializes DD::m未初始化,D是非POD)

  • new D()D-value -initializes ,它调用默认的ctor(D::m未初始化)

  • new E -default-initializes E,它调用comp。gen。ctor。(E::m未初始化,E为非POD)

  • new E() -值初始化E,其零初始化E因为在2点[dcl.init] / 8

  • new F -default-initializes F,它调用comp。gen。ctor。(F::m未初始化,F是非POD)

  • new F() -值初始化F,其缺省初始化 F自1点[dcl.init] / 8Fctor的功能是用户提供的,如果它是用户声明的和未明确拖欠或在其第一声明删除。链接



1
据我所知,在这些示例中,C ++ 98和C ++ 03之间仅存在区别。该问题似乎已在N1161(该文档的更高版本)和CWG DR#178中进行了描述。由于新功能和新的POD规范,C ++ 11中的措词需要更改,而由于C ++ 11措辞中的缺陷,在C ++ 14中再次更改了措辞,但是在这些情况下的效果没有改变。
dyp

3
虽然无聊,struct D { D() {}; int m; };可能值得将其包括在您的列表中。
Yakk-Adam Nevraumont

Answers:


24

C ++ 14指定new在[expr.new] / 17中创建的对象的初始化(在C ++ 11中为[expr.new] / 15,并且该注释不是注释,而是当时的规范文本):

新表达式创建类型的对象T如下初始化该对象:

  • 如果省略了new-initializer,则默认初始化该对象(8.5)。[注意:如果不执行任何初始化,则该对象具有不确定的值。—尾注]
  • 否则,将根据8.5的初始化规则对new-initializer进行解释以进行直接初始化

默认初始化在[dcl.init] / 7中定义(在C ++ 11中为/ 6,措辞本身具有相同的效果):

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

  • 如果T是(可能是经过cv限定的)类类型(第9条),T则调用for的默认构造函数(12.1)(如果T没有默认构造函数或重载分辨率(13.3),则初始化不正确,导致歧义或in从初始化上下文中删除或无法访问的功能);
  • 如果T是数组类型,则每个元素都将默认初始化
  • 否则,不执行初始化。

从而

  • new A只会导致A默认构造函数被调用,而不初始化m。不确定的价值。应该是相同的new B
  • new A() 根据[dcl.init] / 11(在C ++ 11中为/ 10)解释:

    一个其初始化程序是一组空括号(即)的对象(),应进行值初始化。

    现在考虑[dcl.init] / 8(在C ++ 11†中为/ 7):

    值初始化的类型的对象T是指:

    • 如果T是(可能是cv限定的)类类型(第9条),没有默认构造函数(12.1)或用户提供或删除的默认构造函数,则该对象将被默认初始化;
    • 如果T是没有用户提供或删除的默认构造函数的(可能是cv限定的)类类型,则将该对象初始化为零,并检查默认初始化的语义约束,并且如果T具有非平凡的默认构造函数,该对象是默认初始化的;
    • 如果T是数组类型,则每个元素都进行值初始化;
    • 否则,将对象初始化为零。

    因此new A()将零初始化m。这应该A和和等价B

  • new Cnew C()会再次默认初始化该对象,因为使用了最后一个引号的第一个项目符号点(C具有用户提供的默认构造函数!)。但是,很明显,m在两种情况下,现在都是在构造函数中初始化的。


†嗯,此段在C ++ 11中的措词略有不同,因此不会改变结果:

值初始化的类型的对象T是指:

  • 如果T是具有用户提供的构造函数(12.1)的(可能是cv限定的)类类型(第9条),则T 调用的默认构造函数(如果T没有可访问的默认构造函数,则初始化格式不正确);
  • 如果T是没有用户提供的构造函数的(可能是cv限定的)非工会类类型,则该对象将初始化为零,并且如果T隐式声明的默认构造函数非平凡,则将调用该构造函数。
  • 如果T是数组类型,则每个元素都进行值初始化;
  • 否则,将对象初始化为零。

@加布里埃尔不是真的。
2015年

嗯,所以您主要是在谈论c ++ 14,而在括号中给出了c ++ 11的引用
Gabriel

@Gabriel正确。我的意思是,C ++ 14是最新的标准,因此这是最重要的。
哥伦布2015年

1
试图跨标准跟踪初始化规则的烦人的事情是,已发布的C ++ 14和C ++ 11标准之间的很多更改(大部分?全部是通过DR发生的),事实上C ++ 11也是如此。 。然后还有C ++ 14后DR ...-
TC

@Columbo我仍然不明白为什么会struct A { int m; }; struct C { C() : m(){}; int m; };产生不同的结果,以及是什么导致A中的m首先被初始化。我为我所做的实验打开了一个专用线程,在此感谢您的投入以澄清问题。由于stackoverflow.com/questions/45290121/...
darkThoughts

12

以下答案扩展了答案https://stackoverflow.com/a/620402/977038,可作为C ++ 98和C ++ 03的参考

引用答案

  1. 在C ++ 1998中,有两种初始化类型:零和默认
  2. 在C ++ 2003的第三种初始化类型中,添加了值初始化。

C ++ 11(参考n3242)

初始化器

8.5初始化程序[dcl.init]指定变量POD或非POD可以被初始化为大括号或相等的初始化程序,可以将其初始化大括号初始列表,也可以将 初始化子句统称为 大括号或相等。初始化程序或使用(expression-list)。在C ++ 11之前,仅支持(expression-list)initializer-clause,尽管与C ++ 11中的限制相比,对initializer-clause的限制更多。在C ++ 11中,初始化子句现在除了支持括号初始列表赋值表达式就像在C ++ 03中一样。以下语法总结了新支持的子句,其中在C ++ 11标准中新添加了粗体部分。

初始化程序:
    大括号或相等的初始化程序
    (表达式列表)
大括号或相等的初始化程序:
    =初始化程序子句
    括号初始列表
初始化程序子句:
    赋值表达式
    括号初始列表
初始化程序列表:
    初始化子句...选择
    初始化列表,初始化子句...选择**
括号初始化列表:
    {初始化列表,选择}
    {}

初始化

与C ++ 03一样,C ++ 11仍支持三种初始化形式


注意

粗体突出显示的部分已在C ++ 11中添加,删除的部分已从C ++ 11中删除。

  1. 初始值设定项类型:8.5.5 [dcl.init] _zero-initialize_

在以下情况下执行

  • 具有静态或线程存储持续时间的对象被零初始化
  • 如果初始化程序少于数组元素,则每个未显式初始化的元素应被零初始化。
  • 值初始化期间,如果T是(可能是cv限定的)非工会类类型,而没有用户提供的构造函数,则该对象将被初始化为零。

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

  • 如果T是标量类型(3.9),则将对象设置为值0(零),作为整数常量表达式,转换为T;
  • 如果T是(可能是cv限定的)非联合类类型,则将每个非静态数据成员和每个基类子对象初始化为零,并将填充初始化为零位;
  • 如果T是(可能是cv限定的)联合类型,则对象的第一个非静态命名数据成员初始化为零,并且填充初始化为零位;
  • 如果T是数组类型,则每个元素都初始化为零。
  • 如果T是引用类型,则不执行初始化。

2.初始化程序类型:8.5.6 [dcl.init] _default-initialize_

在以下情况下执行

  • 如果省略了new-initializer,则该对象将被默认初始化;否则,该对象将被默认初始化。如果未执行初始化,则该对象具有不确定的值。
  • 如果未为对象指定初始化程序,则该对象将被默认初始化,但具有静态或线程存储持续时间的对象除外
  • 当构造函数初始化器列表中未提及基类或非静态数据成员时,将调用该构造函数。

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

  • 如果T是(可能是cv限定的) 非POD类类型(第9条),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化格式不正确);
  • 如果T是数组类型,则每个元素都将默认初始化;
  • 否则,不执行初始化。

注意在C ++ 11之前,不使用初始化程序时,只有具有自动存储持续时间的非POD类类型才被视为默认初始化。


3.初始化程序类型:8.5.7 [dcl.init] _value-initialize_

  1. 当一个对象(无名的临时变量,命名的变量,动态存储持续时间或非静态数据成员)的初始化程序是一组空括号(即()或大括号{})时

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

  • 如果T是具有用户提供的构造函数(12.1)的(可能是cv限定的)类类型(第9条),则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化格式不正确) ;
  • 如果T是没有用户提供的构造函数的(可能是cv限定的)非工会类类型,则T的每个非静态数据成员和基类组件都将被值初始化; 然后将该对象初始化为零,并且,如果T的隐式声明的默认构造函数不重要,则调用该构造函数。
  • 如果T是数组类型,则每个元素都进行值初始化;
  • 否则,将对象初始化为零。

所以总结一下

注意标准中的相关报价以粗体突出显示

  • 新A:默认初始化(未初始化A :: m)
  • new A():零初始化A,因为值初始化的候选对象没有用户提供或删除的默认构造函数。如果T是(可能是cv限定的)不具有用户提供的构造函数的非工会类类型,则该对象将初始化为零,并且,如果T的隐式声明的默认构造函数非平凡,则将调用该构造函数。
  • 新B:默认初始化(叶子B :: m未初始化)
  • new B():将B值初始化,将所有字段初始化为零;如果T是具有用户提供的构造函数(12.1)的(可能是cv限定的)类类型(第9条),则调用T的默认构造函数
  • new C:默认初始化C,该C调用默认ctor。如果T是(可能是cv限定的)类类型(第9条),则调用T的默认构造函数,此外,如果省略new-initializer,则该对象将被默认初始化
  • new C():值初始化C,它将调用默认的ctor。如果T是具有用户提供的构造函数(12.1)的(可能是cv限定的)类类型(第9条),则将调用T的默认构造函数。此外,其初始化程序是一个空括号集合(即())的对象应进行值初始化。

0

我可以确认,在C ++ 11中,至少在编译器实现方面,C ++ 14问题中提到的所有内容都是正确的。

为了验证这一点,我在测试套件中添加了以下代码。我-std=c++11 -O3在GCC 7.4.0,GCC 5.4.0,Clang 10.0.1和VS 2017中进行了测试,下面的所有测试都通过了。

#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    { FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    { FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    { FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    { FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    { FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    { FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

UB!提到的地方是未定义的行为,实际行为可能取决于许多因素(a.m可能等于42、0或其他垃圾)。从~UB理论上讲,这些位置也是未定义的行为,但在实践中,由于使用了新的位置,因此a->m与42以外的任何其他位置相比,它的可能性极小。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.