值初始化:默认初始化还是零初始化?

19

我已经设计了一个 gray_code 类,用于存储一些无符号整数,其底层位以格雷码顺序存储。以下是代码:

template<typename UnsignedInt>
struct gray_code
{
    static_assert(std::is_unsigned<UnsignedInt>::value,
                  "gray code only supports built-in unsigned integers");

    // Variable containing the gray code
    UnsignedInt value;

    // Default constructor
    constexpr gray_code()
        = default;

    // Construction from UnsignedInt
    constexpr explicit gray_code(UnsignedInt value):
        value( (value >> 1) ^ value )
    {}

    // Other methods...
};

在一些通用算法中,我写了以下类似的代码:

template<typename UnsignedInt>
void foo( /* ... */ )
{
    gray_code<UnsignedInt> bar{};
    // Other stuff...
}

在这段代码中,我期望bar被零初始化,因此bar.value也应该被零初始化。然而,在遇到意外错误后,我发现bar.value被初始化为垃圾值(确切地说是4606858),而不是0u。这让我感到惊讶,所以我去cppreference.com看一下上面的代码应该做什么...
根据我的理解,形式为T object {};对应于value initialization。我发现这句话很有趣:
“在所有情况下,如果使用空括号对{}并且T是集合类型,则执行集合初始化而不是值初始化。”
然而,gray_code有一个用户提供的构造函数。因此它不是一个聚合体,因此不执行aggregate initializationgray_code没有采用std::initializer_list的构造函数,因此也不执行list initialization。然后,gray_code的值初始化应遵循C++14的通常规则:
1)如果T是一个没有默认构造函数或带有用户提供的默认构造函数或删除的默认构造函数的类类型,则对象将被默认初始化。
2)如果T是一个没有用户提供或删除的默认构造函数的类类型(即可能是具有默认构造函数或隐式定义的类),则对象将被零初始化,然后如果它具有非平凡的默认构造函数,则进行默认初始化。
3)如果T是数组类型,则数组的每个元素都会被值初始化。
4)否则,对象将被零初始化。
如果我理解正确,gray_code具有显式默认(非用户提供的)默认构造函数,因此不适用1)。它具有默认的默认构造函数,因此适用2):gray_codezero-initialized。默认的默认构造函数似乎符合平凡默认构造函数的所有要求,因此不应发生默认初始化。让我们看一下gray_code如何被零初始化:
如果T是标量类型,则对象的初始值为将积分常数零隐式转换为T。如果T是非联合类类型,则所有基类和非静态数据成员都将被初始化为零,并且所有填充都将初始化为零位。构造函数(如果有)将被忽略。如果T是联合类型,则第一个非静态命名数据成员将被初始化为零,并且所有填充都将初始化为零位。如果T是数组类型,则每个元素都将被初始化为零。如果T是引用类型,则不执行任何操作。 gray_code是非联合类类型。因此,应该初始化其所有非静态数据成员,这意味着value将被初始化为零。value满足std :: is_unsigned,并且因此是标量类型,这意味着它应该使用“将积分常数零隐式转换为T”进行初始化。
因此,如果我正确地阅读了所有内容,则在上面的foo函数中,bar.value应始终使用0进行初始化,并且永远不应使用垃圾进行初始化,我是否正确?
注意:我编译代码的编译器是MinGW_w4 GCC 4.9.1(带有POSIX线程和dwarf异常),以便于帮助。虽然我有时会在计算机上获得垃圾,但我从未能够在在线编译器中获得除零以外的任何内容。
class foo {
    foo() = default;
};

并且

class foo {
    foo();
};

foo::foo() = default;

“等效”和“相等”是不同的,以下是来自C++14标准中[dcl.fct.def.default]章节的引用:

如果一个函数在第一次声明时没有被显式地默认或删除,则该函数是“用户提供的”,前提是它已经被用户声明。

换句话说,当我得到垃圾值时,我的默认默认构造函数确实是用户提供的,因为它在第一次声明时没有被显式地默认。因此,发生的不是零初始化,而是默认初始化。再次感谢@Columbo指出真正的问题。


4
如果一个函数在其第一次声明时没有显式地被默认或删除,则它是由用户提供的,即使它是由用户声明的。你的构造函数不是由用户提供的。 - user657267
1
一个注记... 值初始化在微软的编译器上仍然无法正常工作,包括VS2013。 - Mgetz
这个问题并没有展示出来,这些初始化规则真的很丑陋吗? 特别是从c++03到c++11发生了什么,那些本应该进行值初始化的代码在c++11中不再执行相同的操作了? - Gabriel
@Gabriel иҝҷеҸӘжҳҜиЎЁжҳҺеңЁC++11дёӯж·»еҠ зҡ„= defaultе…·жңүдёҖдәӣеҘҮжҖӘзҡ„规еҲҷгҖӮеңЁC++03дёӯжү§иЎҢеҖјеҲқе§ӢеҢ–зҡ„д»Јз Ғд»Қ然еңЁC++11дёӯжү§иЎҢеҖјеҲқе§ӢеҢ–гҖӮ - Morwenn
上述行为很粗糙, :-) 希望标准委员会有一些好的理由来解释这个奇怪的规则。对我来说,这是一个完全不合逻辑的例外。 - Gabriel
显示剩余6条评论
1个回答

10
所以,如果我理解正确,在上面的函数foo中,bar.value应该始终使用0进行初始化,而不应该使用垃圾值进行初始化,是吗?
是的。你的对象是直接列表初始化的。 C ++14 * [dcl.init.list] / 3指定如下:
对类型为T的对象或引用的列表初始化定义如下:
在以下情况下执行聚合初始化(8.5.1)。)
否则,如果初始化程序列表没有元素并且T是具有默认构造函数的类类型,则对象将进行值初始化。
* […不适用的项目…]
否则,如果T是聚合体,则执行聚合初始化。
否则,如果初始化程序列表没有元素并且T是具有默认构造函数的类类型,则对象将进行值初始化。
[dcl.init] / 7:
对于类型为T的对象进行value-initialize意味着:
如果T是一个(可能具有cv限定符的)类类型(第9条),它没有默认构造函数(12.1)或者具有用户提供或删除的默认构造函数,则对象将进行默认初始化;
如果T是一个(可能具有cv限定符的)类类型,没有用户提供或删除默认构造函数,则对象将进行零初始化,并检查默认初始化的语义约束,如果T具有非平凡的默认构造函数,则对象将进行默认初始化;
[dcl.fct.def.default] / 4:
如果特殊成员函数是用户声明的且在其第一次声明时未显式默认化[…],则该特殊成员函数是用户提供的。
因此,你的构造函数不是用户提供的,因此对象被初始化为0。(由于其是平凡的,因此不会调用构造函数)
最后,以防不清楚,将类型为T的对象或引用进行零初始化意味着:
因此,要么您的编译器有错误,要么您的代码在某个其他点触发了未定义行为。
* 在C++11中仍然是肯定的答案,尽管引用的部分不等同。

我查看了生成的汇编代码。默认构造函数只有一条有意义的指令,即 mov %ecx,-0x4(%ebp)。如果我理解正确,它设置了 bar 的地址,但从未设置 value - Morwenn
@Morwenn 当然构造函数不会设置value。隐式生成的默认构造函数对成员对象进行默认初始化,这意味着标量没有进行初始化。 - Columbo
你说得对,似乎我混淆了构造函数和初始化部分,话说,在我的情况下调用了默认的默认构造函数。但是,这个调用点附近似乎没有任何其他东西来初始化value - Morwenn
@Morwenn 是的,那是由于该 bug。 - Columbo
我真的希望相信这是一个 bug,但我无法产生一个最小的测试用例,使得value被默认初始化而不是零初始化。不管怎样,还是非常感谢你的帮助 :) - Morwenn
显示剩余2条评论

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