考虑以下代码:
struct X{
explicit X(){}
explicit X(const X&){}
};
void foo(X a = X()){}
int main(){}
使用C ++ 14标准,GCC 7.1和clang 4.0 rejects代码,这正是我所期望的。
但是,使用C ++ 17(-std=c++1z
),they both accept代码。什么规则改变了?
对于两个编译器都表现出同样的行为,我怀疑这是一个错误。但据我所知,最新的草案仍然说,默认参数使用 copy-initialization 1 的语义。同样,我们知道explicit
构造函数只允许直接初始化 2 。
答案 0 :(得分:10)
因为copy elision的行为改变了C ++ 17;对于这种情况,优化是强制性的。
在下列情况下,编制者需要省略 类对象的复制和移动构造函数,即使复制/移动也是如此 构造函数和析构函数具有可观察到的副作用:
在初始化中,如果初始化表达式是prvalue而且是 cv-源类型的非限定版本与该类相同 目的地的类,初始化表达式用于 初始化目标对象:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
复制初始化的效果是:
首先,如果
T
是类类型且初始化程序是prvalue 表达式的cv-unqualified类型与T
是同一类, 初始化表达式本身,而不是一个临时的物化 从中,用于初始化目标对象:请参阅copy elision(自C ++ 17起)如果
T
是类类型且类型为cv-nonqualified的版本 另一个是T
或派生自T
的类,非显式构造函数 检查T
并通过重载决策选择最佳匹配。 然后调用构造函数初始化对象。
这意味着X a = X()
,a
将直接默认构建,复制/移动构造函数及其副作用将完全被忽略。不会发生用于重载解析的非显式构造函数的选择,这在C ++ 14(以及之前)中是必需的。对于这些保证的情况,复制/移动构造函数不参与,那么它们是否explicit
无关紧要。
答案 1 :(得分:1)
对于问题规则中的示例而言,最重要的是[expr.type.conv] / 2。但让我们从[dcl.init]/17开始:
初始化器的语义如下。目标类型是要初始化的对象或引用的类型,源类型是初始化器表达式的类型。如果初始化程序不是单个(可能带有括号)表达式,则不会定义源类型。
...
(17.6)-如果目标类型是(可能具有cv资格的)类类型:
-如果初始化程序表达式为prvalue,并且源类型的cv不合格版本与目标程序的类相同,则使用初始化程序表达式初始化目标对象。 [示例:
T x = T(T(T()));
调用T
的默认构造函数来初始化x
。 — 结束示例]
因此,在X a = X()
中,初始化器表达式X()
用于初始化目标对象。当然,这还不足以回答:为什么选择默认构造函数(即X()
变成()
的原因以及为什么explicit
默认构造函数很好)。
X()
表达式是功能符号中的显式类型转换,因此让我们看一下[expr.type.conv]/2:
如果初始化程序是一个带括号的单个表达式,则类型转换表达式(在定义上,如果定义了含义)与相应的强制转换表达式等效。如果类型为cv void,并且初始值设定项为(),则表达式是不执行初始化的指定类型的prvalue。 否则,表达式是指定类型的prvalue,其结果对象使用初始化程序直接初始化。
相关句子的重点是我的。它说X()
:
使用()
初始化对象([expr.type.conv] / 1是“初始值设定项”),这就是选择默认构造函数的原因;
该对象是直接初始化的,因此可以确定默认构造函数为explicit
。
更多详细信息:当初始化程序为()
时,将应用[dcl.init]/(17.4):
如果初始化器为
()
,则对象将被值初始化。
对于value-initialize类型为T的对象,其含义是:
—如果T
是(可能是cv限定的)类类型,没有默认构造函数([class.ctor])或用户提供或删除的默认构造函数,则该对象是默认初始化的; < / p>
对于default-initialize类型为
T
的对象,其含义是:
—如果T
是(可能是cv限定的)类类型,则考虑构造函数。列举适用的构造函数([over.match.ctor]),并通过overload resolution选择最适合初始化器()
的构造函数。如此选择的构造函数将被调用,并带有一个空的参数列表以初始化对象。
当直接初始化类类型的对象,从相同或派生类类型的表达式([dcl.init])的副本复制初始化或默认初始化时,重载决议会选择构造函数。 对于不在复制初始化上下文中的直接初始化或默认初始化,候选函数是要初始化的对象的类的所有构造函数。。 / p>
在C ++ 14中,[dcl.init](17.6)说:
-如果目标类型是(可能是cv限定的)类类型:
-如果初始化是直接初始化,或者如果复制是初始化,其中源类型的cv不合格版本与目标的类相同,或为该类的派生类,则考虑构造函数。枚举适用的构造函数([over.match.ctor]),并通过重载分辨率([over.match])选择最佳的构造函数。
因此对于X a = X()
,将仅考虑接受一个类型为explicit
的一个参数的转换(非X
)构造函数(即复制和移动构造函数)。