从零开始
首先,我们将澄清何为初始化对象以及构造函数何时/如何/是否被调用。
以下是我对标准的通俗解释,为了简洁起见,省略或修改了一些无关紧要的细节。
初始化器
初始化器有以下几种类型:
() // parentheses
// nothing
{} // braced initializer list
= expr // assignment expression
括号和大括号初始化列表可能包含进一步的表达式。它们的使用方式如下所示,假设有结构体S.
new S() // empty parentheses
S s(1, 2) // parentheses with expression list as (1, 2)
S s // nothing
S s{} // empty braced initializer list
S s{{1}, {2}} // braced initializer list with sublists
S s = 1 // assignment
S s = {1, 2} // assignment with braced initializer list
请注意,我们尚未提到构造函数
初始化
根据使用的初始化程序执行初始化。
new S() // value-initialize
S s(1, 2) // direct-initialize
S s // default-initialize
S s{} // list-initialize
S s{{1}, {2}} // list-initialize
S s = 1 // copy-initialize
S s = {1, 2} // list-initialize
一旦初始化完成,该对象被视为已初始化。
请注意,构造函数再次未被提及
List initialize
我们将主要解释什么是列表初始化,因为这是问题所在。
当发生列表初始化时,以下按顺序考虑:
- 如果对象是聚合类型,且列表具有一个单独的元素,该元素是对象类型或源自对象类型,则使用该元素初始化对象。
- 如果对象是聚合类型,则进行聚合初始化。
- 如果列表为空,并且对象具有默认构造函数,则该对象进行值初始化(最终调用默认构造函数)。
- 如果对象是类类型,则考虑构造函数,使用列表的元素执行重载决策。
Aggregate
聚合类型定义如[dcl.init.aggr]所述:
聚合是具有以下特征的数组或类:
--没有用户提供的、显式的或继承的构造函数
--没有非静态数据成员的私有或保护成员
--没有虚函数和虚拟、私有或保护基类
具有删除的构造函数不计入提供构造函数的部分。
聚合的元素定义如下:
聚合的元素有:
--对于数组,是按递增下标顺序的数组元素;或者
--对于类,是按声明顺序的直接基类,后跟未成为匿名联合成员的直接非静态数据成员,按声明顺序排列。
聚合初始化定义如下:
[...]初值列表的元素将按顺序作为聚合元素的初始值。
Example e{}
示例
遵循上述规则,Example e{}
之所以合法,是因为
the initializer is a braced initializer list
uses list initialization
since Example is an aggregate type
uses aggregate initialization
and therefore does not invoke any constructor
当你编写
Example e{}
时,它并没有进行默认构造,而是聚合初始化。所以,没问题。
事实上,以下内容可以编译通过。
struct S
{
S() = delete;
S(const S&) = delete;
S(S&&) = delete;
S& operator=(const S&) = delete;
};
S s{};
关闭构造函数
确保Example
不是一个聚合类型,以停止聚合初始化并删除其构造函数。
这通常很简单,因为大多数类具有私有或受保护的数据成员。因此,在C++中经常忘记了聚合初始化存在的事实。
使一个类非聚合的最简单方法是:
struct S
{
explicit S() = delete;
};
S s{};
然而,截至2017年5月30日,只有gcc 6.1及以上版本和clang 4.0.0会拒绝此代码,所有版本的CL和icc都将错误地接受此代码。
其他初始化
这是C++中最疯狂的角落之一,查阅标准以理解发生了什么事情是相当费劲但也具有启发性的。已经有许多参考资料可供阅读,我不打算再尝试解释它们。