对C ++的哪些更改使得使用显式构造函数的类进行了复制初始化工作?

时间:2017-05-24 09:24:08

标签: c++ c++14 language-lawyer c++17 explicit

考虑以下代码:

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

1 dcl.fct.default/5; 2 class.conv.ctor/2

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
    
  •   

copy initialization

  

复制初始化的效果是:

     
      
  • 首先,如果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)

如果初始化器为(),则对象将被值初始化。

[dcl.init]/8

对于value-initialize类型为T的对象,其含义是:
—如果T是(可能是cv限定的)类类型,没有默认构造函数([class.ctor])或用户提供或删除的默认构造函数,则该对象是默认初始化的; < / p>

[dcl.init]/7

对于default-initialize类型为T的对象,其含义是:
—如果T是(可能是cv限定的)类类型,则考虑构造函数。列举适用的构造函数([over.match.ctor]),并通过overload resolution选择最适合初始化器()的构造函数。如此选择的构造函数将被调用,并带有一个空的参数列表以初始化对象。

[over.match.ctor]/1

当直接初始化类类型的对象,从相同或派生类类型的表达式([dcl.init])的副本复制初始化或默认初始化时,重载决议会选择构造函数。 对于不在复制初始化上下文中的直接初始化或默认初始化,候选函数是要初始化的对象的类的所有构造函数。。 / p>


在C ++ 14中,[dcl.init](17.6)说:

-如果目标类型是(可能是cv限定的)类类型:

-如果初始化是直接初始化,或者如果复制是初始化,其中源类型的cv不合格版本与目标的类相同,或为该类的派生类,则考虑构造函数。枚举适用的构造函数([over.match.ctor]),并通过重载分辨率([over.match])选择最佳的构造函数。

因此对于X a = X(),将仅考虑接受一个类型为explicit的一个参数的转换(非X)构造函数(即复制和移动构造函数)。