我最近了解到,一个类中可以有多个默认构造函数。然后我写了下面的程序,用 msvc 编译,但是 clang 和 gcc 都无法编译它。
struct A
{
explicit A(int = 10);
A()= default;
};
A a = {}; //msvc ok but gcc and clang fails here
我想知道根据 C++17 标准,哪个编译器是正确的。
海湾合作委员会说:
<source>:8:8: error: conversion from '<brace-enclosed initializer list>' to 'A' is ambiguous
8 | A a = {}; //msvc ok but gcc and clang fails here
| ^
<source>:5:3: note: candidate: 'constexpr A::A()'
5 | A()= default;
| ^
<source>:4:12: note: candidate: 'A::A(int)'
4 | explicit A(int = 10);
TLDR;
该程序是格式良好,符合如下所述的标准。 基本上,只有一个构造函数(
A::A()
和 explicit A::A(int)
)是转换构造函数。因此,只有前者 A::A()
是可行的选项,因此可以使用。
首先注意
A a = {};
是复制初始化。
- 以大括号或等于初始化器或条件([stmt.select])的 = 形式发生的初始化,以及参数传递、函数返回、抛出异常([ except.throw]) 、处理异常([except.handle])和聚合成员初始化([dcl.init.aggr]),称为复制初始化。
接下来我们转向初始化器的语义。
- 初始化器的语义如下。目标类型是正在初始化的对象或引用的类型,源类型是初始化表达式的类型。如果初始值设定项不是单个(可能带括号)表达式,则未定义源类型。
- 如果初始化器是(非括号)braced-init-list 或 is = braced-init-list,则对象或引用是列表初始化的 ([dcl.init.list])。
上面的意思是,该对象将被列表初始化。
来自列表初始化:
列表初始化是从大括号初始化列表中初始化对象或引用。 这样的初始值设定项称为初始值设定项列表,初始值设定项列表的逗号分隔初始值设定项子句或指定初始值设定项列表的指定初始值设定项子句称为初始值设定项列表的元素。 初始值设定项列表可能为空。列表初始化可以发生在直接初始化或复制初始化上下文中;直接初始化上下文中的列表初始化称为直接列表初始化,而复制初始化上下文中的列表初始化称为复制列表初始化。
上面的意思是
A a = {};
是复制列表初始化。接下来我们看列表初始化的效果:
- 类型 T 的对象或引用的列表初始化定义如下:
- 否则,如果初始化器列表没有元素并且 T 是具有默认构造函数的类类型,则该对象将被值初始化。
上面的意思是该对象将被值初始化,所以我们继续进行值初始化:
- 对 T 类型的对象进行值初始化意味着:
- 如果 T 没有默认构造函数([class.default.ctor])或用户提供的 默认构造函数或被删除,则该对象被默认初始化;
这意味着该对象将被默认初始化:
- 默认初始化 T 类型的对象意味着:
- 如果 T 是(可能是 cv 限定的)类类型 ([class]),则考虑构造函数。 枚举适用的构造函数 ([over.match.ctor]),并通过重载决策 ([over.match]) 选择
的最佳构造函数。使用空参数列表调用如此选择的构造函数来初始化对象。initializer ()
因此我们继续进行over.match.ctor以获得候选ctors列表。另请注意上面引用的参考文献中的
initializer()
部分,因为它将在最后用作参数。
- 当类类型的对象被直接初始化、从相同或派生类类型 ([dcl.init]) 的表达式复制初始化或默认初始化时,重载决策会选择构造函数。对于不在复制初始化上下文中的直接初始化或默认初始化,候选函数是正在初始化的对象的类的所有构造函数。 对于复制初始化,候选函数是该类的所有转换构造函数。参数列表是初始化器的表达式列表或赋值表达式。
这意味着只有非显式 ctor
A::A()
是候选者,因为它是转换 ctor,而另一个 explicit A::A(int)
则不是。因此,候选者和可行选项集仅包含一个 ctor A::A()
。
最后,由于我们只有一个可行的选项,因此它就是为初始化器 () 选择的选项。