【问题标题】:Default, value and zero initialization mess默认值、值和零初始化混乱
【发布时间】:2015-06-28 05:53:29
【问题描述】:

我对值、默认值和零初始化感到非常困惑。 尤其是当他们加入不同的标准C++03C++11(和C++14)时。 p>

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

简而言之,通过示例获得完整见解:

有时 new 操作符返回的内存会被初始化,有时它不会取决于你要更新的类型是POD (plain old data),或者它是否是一个包含 POD 成员的类并且正在使用编译器生成的默认构造函数。

  • C++1998 中有 2 种初始化类型:zero-default-initialization
  • C++2003 中添加了第三种初始化类型,值初始化
  • C++2011/C++2014 中仅添加了 list-initializationvalue-/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() - value-initialize A,这是一个零初始化,因为它是一个 POD。
  • new B - 默认初始化(使 B::m 未初始化,B 是非 POD)
  • new B() - value-initializes B 将所有字段初始化为零,因为它的默认 ctor 是编译器生成的,而不是用户定义的。
  • new C - 默认初始化 C,它调用默认 ctor。 (C::m 是零初始化,C 是非 POD)
  • new C() - 值初始化 C,它调用默认 ctor。 (C::m 初始化为零)
  • new D - 默认构造(D::m 未初始化,D 非 POD)
  • new D() - value-initializes D?,它调用了默认的 ctor(D::m 未初始化)

斜体值和 ?是不确定性,请帮助纠正这个:-)

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

??? (请帮忙,如果我从这里开始,无论如何都会出错)

在符合 C++14 的编译器中,事情应该是这样的: ??? (如果我从这里开始,请帮忙,它无论如何都会出错) (根据答案草稿)

  • new A - 默认初始化A,编译器生成。 ctor,(离开 A::m 未初始化)(A 是 POD)

  • new A() - value-initializes A,这是自 2. 以来的零初始化。指向 [dcl.init]/8

  • new B - 默认初始化B,编译器生成。 ctor,(离开 B::m 未初始化)(B 是非 POD)

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

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

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

  • new D - 默认初始化 DD::m 未初始化,D 非 POD)

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

  • new E - 默认初始化 E,它调用 comp.将军医生。 (E::m 未初始化,E 非 POD)

  • new E() - value-initializes E,从 [dcl.init]/8 的 2 点开始将 E 初始化为零)

  • new F - 默认初始化 F,它调用 comp.将军医生。 (F::m 未初始化,F 非 POD)

  • new F() - value-initializes F, default-initializes F 因为 1. 指向 [dcl.init]/8 (@987654407 @ctor 函数是用户提供的,如果它是用户声明的,并且在第一次声明时没有显式默认或删除。Link)

【问题讨论】:

  • 据我所知,在这些示例中,C++98 和 C++03 之间只有区别。该问题似乎在N1161(该文档有后续修订版)和CWG DR #178 中有所描述。由于 POD 的新特性和新规范,措辞需要在 C++11 中更改,并且由于 C++11 措辞中的缺陷,它在 C++14 中再次更改,但是这些情况下的效果不会改变。
  • 虽然很无聊,但struct D { D() {}; int m; }; 可能值得包含在您的列表中。
  • 有一张很好的令人不安的海报,它把这个混乱带到了重点:randomcat.org/cpp_initialization/initialization.png

标签: c++ c++11 c++14 c++03 c++98


【解决方案1】:

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

创建T 类型对象的new-expression 初始化该对象 对象如下:

  • 如果 new-initializer 被省略,则对象为 default-initialized (8.5)。 [ 注意: 如果没有初始化 执行时,对象具有不确定的值。 ——尾注 ]​​i>
  • 否则,new-initializer 会根据 8.5 的初始化规则来解释 direct-initialization

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

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

  • 如果T 是(可能是cv 限定的)类类型(第9 条),则调用T 的默认构造函数(12.1)(并且初始化 如果T 没有默认构造函数或重载决议,则格式错误 (13.3) 导致歧义或功能被删除或 无法从初始化的上下文中访问);
  • 如果T是数组类型,每个元素都是默认初始化的
  • 否则,不执行初始化。

这样

  • new A 仅导致调用 As 默认构造函数,它不会初始化 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 进行零初始化。这应该等同于AB

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


† 好吧,这一段在 C++11 中的措辞略有不同,但不会改变结果:

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

  • 如果T 是(可能是 cv 限定的)类类型(第 9 条),带有 用户提供的构造函数 (12.1),然后是 T 的默认构造函数 被调用(如果 T 不可访问,则初始化格式错误 默认构造函数);
  • 如果T 是(可能是 cv 限定的)非联合 没有用户提供的构造函数的类类型,那么对象是 零初始化,如果 T 的隐式声明的默认构造函数 是不平凡的,该构造函数被调用。
  • 如果T是一个数组类型, 然后每个元素都进行值初始化;
  • 否则,对象为 零初始化。

【讨论】:

  • 啊,所以你说的主要是c++14,括号中给出了c++11的引用
  • @Gabriel 正确。我的意思是,C++14 是最新的标准,所以它是最突出的。
  • 尝试跨标准跟踪初始化规则的恼人之处在于,已发布的 C++14 和 C++11 标准之间的许多更改(大部分?全部?)都是通过 DR 发生的,并且事实上的 C++11 也是如此。然后还有 C++14 后的 DRs...
  • @Columbo 我仍然不明白为什么struct A { int m; }; struct C { C() : m(){}; int m; };会产生不同的结果,以及是什么导致 A 中的 m 首先被初始化。我已经为我所做的实验打开了一个专门的线程,我将感谢您在那里的输入以澄清问题。谢谢stackoverflow.com/questions/45290121/…
【解决方案2】:

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

引用答案

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

C++11(参考 n3242)

初始化器

8.5 Initializers [dcl.init] 指定变量 POD 或非 POD 可以被初始化为 brace-or-equal-initializer 可以是 braced-init-list 或 initializer-clause 统称为 brace-or-equal-initializer 或使用 ( 表达式列表 )。在 C++11 之前,只支持 (expression-list)initializer-clause 虽然 initializer-clause 比我们的限制更多在 C++11 中有。在 C++11 中,除了 C++03 中的 assignment-expression 之外,initializer-clause 现在支持 braced-init-list。以下语法总结了新支持的子句,其中粗体部分是C++11标准中新增的。

初始化器:
&nbsp&nbsp&nbsp&nbsp大括号或等号初始化器
&nbsp&nbsp&nbsp&nbsp( 表达式列表 )
大括号或等于初始化器:
&nbsp&nbsp&nbsp&nbsp= 初始化子句
&nbsp&nbsp&nbsp&nbsp支撑初始化列表
初始化子句:
&nbsp&nbsp&nbsp&nbsp赋值表达式
&nbsp&nbsp&nbsp&nbsp支撑初始化列表
初始化列表:
&nbsp&nbsp&nbsp&nbsp初始化子句 ...opt
&nbsp&nbsp&nbsp&nbspinitializer-list , initializer-clause ...opt**
braced-init-list:
&nbsp&nbsp&nbsp&nbsp{ 初始化列表 ,opt }
&nbsp&nbsp&nbsp&nbsp{ }

初始化

和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-qualified) 非联合类类型,则每个非静态数据成员和每个基类子对象都是零初始化 并且填充被初始化为零位;
  • 如果 T 是 (可能是 cv 限定) 联合类型,则对象的第一个非静态命名数据成员为零初始化和填充初始化为零位;
  • 如果 T 是数组类型,则每个元素都初始化为零;
  • 如果 T 是引用类型,则不执行初始化。

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

在以下情况下执行

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

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

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

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


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

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

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

  • 如果 T 是具有用户提供的构造函数 (12.1) 的 (可能是 cv-qualified) 类类型(第 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 的默认构造函数
  • 新 C :默认初始化 C,它调用默认 ctor。 如果 T 是(可能是 cv 限定的)类类型(第 9 条),则调用 T 的默认构造函数,此外,如果省略 new-initializer,则对象是默认初始化的
  • new C() :值初始化 C,它调用默认 ctor。 如果 T 是具有用户提供的构造函数 (12.1) 的(可能是 cv 限定的)类类型(第 9 条),则调用 T 的默认构造函数。 , 初始化器为空括号集的对象,即 (),应进行值初始化

【讨论】:

    【解决方案3】:

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

    为了验证这一点,我将以下代码添加到我的test suite。我在 GCC 7.4.0、GCC 5.4.0、Clang 10.0.1 和 VS 2017 中使用 -std=c++11 -O3 进行了测试,以下所有测试均通过。

    #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).
    //
    // Update 2020-12-14: Even the less aggressive EXPECT_NE(a.m, 0) fails in some
    // machines, so we comment them out. But this change in behavior does illustrate
    // that, in fact, the behavior was undefined.
    //
    #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-&gt;m 不太可能等于42 以外的任何值。

    【讨论】:

    • 即使使用 GCC7.4,我的某些测试似乎也失败了:godbolt.org/z/PocEGz6sq... 可能是 GTest 版本很重要吗?这很奇怪,因为代码似乎是您在 GitHub 上的测试套件的一部分,测试仍然通过(或至少 5 个月前通过?)
    • @AdomasBaliuka 感谢您的提醒。我很快就会回来编写代码,所以我会看看测试是否仍然通过。我记得有些测试在某些平台上没有通过,我不得不进行更改,不确定是在这篇 SO 帖子之前还是之后。请注意,我现在使用的是 C++17,因此结果也可能因此而有所不同。
    • @AdomasBaliuka 我添加了一个 GitHub 问题来跟踪这个,如果你有兴趣的话:github.com/vgc/vgc/issues/566
    • @AdomasBaliuka 我刚刚有时间看看这个。你是对的,测试标记为“UB!”在某些机器/编译器中确实失败了。事实上,早在 2020 年 12 月 14 日(github.com/vgc/vgc/commit/1d3f492e6),我就已经在我的生产代码库中将它们注释掉了,这也是我的测试通过的原因。这个故事的精神是:那些绝对是未定义的行为,所以,好吧,行为是未定义的。该值不应该是零初始化的,但它可能由于“机会”或其他因素而发生为零。
    猜你喜欢
    • 2014-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-09
    • 2013-10-08
    • 1970-01-01
    相关资源
    最近更新 更多