复制列表初始化和传统复制初始化之间的任何区别

Any difference between copy-list-initialization and traditional copy-initialization?

本文关键字:复制 初始化 区别 任何 传统 列表 之间      更新时间:2023-10-16

除了支持多个参数、不允许缩小转换、匹配构造函数采用std::initializer_list参数外,复制列表初始化与传统的复制初始化还有什么不同?

具体来说,假设有两种用户定义的类型,AB:

class A {...};
class B {...};
B b;
A a1 = {b};
A a2 = b;

AB的什么定义会对这两种初始化形式产生影响?例如,AB是否有某种定义,会使初始化中的一个合法但另一个非法,或者两者合法但语义不同,或者两者因不同原因而非法?

(假设A没有采用std::initializer_list参数的构造函数。)

编辑:添加一个链接到我的一个有点相关的问题:在带有转换运算符的初始值设定项的情况下,复制列表初始化的假定行为是什么?

复制初始化总是考虑复制构造函数的可用性,而复制列表初始化则不考虑。
class B {};
struct A 
{
A(B const&) {}
A(A const&) = delete;
};
B b;
A a1 = {b};  // this compiles
A a2 = b;    // this doesn't because of deleted copy-ctor

这是因为复制列表初始化与直接列表初始化相同,但有一种情况除外——如果A(B const&)explicit,前者会失败,而后者会工作。

class B {};
struct A 
{
explicit A(B const&) {}
};

int main()
{
B b;
A a1{b};    // compiles
A a2 = {b}; // doesn't compile because ctor is explicit
}

可能,新副本列表初始化的行为被定义为"良好"和一致,但由于向后兼容性,旧副本初始化的"奇怪"行为无法改变
正如您所看到的,此子句中的列表初始化规则对于直接表单和复制表单是相同的
explicit相关的差异仅在过载解决方案一章中描述。但对于传统的初始化,直接形式和复制形式是不相同的
传统初始化和大括号初始化是分别定义的,因此总是存在一些(可能是无意的)细微差异。

我可以从标准的摘录中看到差异:

1.已经提到的差异

  • 不允许缩小转换范围
  • 可以有多个参数
  • braked语法更喜欢初始值设定项列表构造函数(如果存在):

    struct A
    {
    A(int i_) : i (i_) {}
    A(std::initializer_list<int> il) : i (*il.begin() + 1) {}
    int i;
    }
    A a1 = 5; // a1.i == 5
    A a2 = {5}; // a2.i = 6
    


2.骨料的不同行为

对于聚合,您不能使用支撑副本构造函数,但可以使用传统的构造函数。

struct Aggr
{
int i;
};
Aggr aggr;
Aggr aggr1 = aggr; // OK
Aggr aggr2 = {aggr}; // ill-formed


3.存在转换运算符时参考初始化的不同行为

Brace初始化不能使用转换为引用类型的运算符

struct S
{
operator int&() { return some_global_int;}
};
int& iref1 = s; // OK
int& iref2 = {s}; // ill-formed


4.其他类型的对象初始化类类型的对象时的一些细微差异

这些差异在本答案末尾的标准摘录中用[*]标记。

  • 旧的初始化使用用户定义的转换序列的概念(特别是,如前所述,需要副本构造函数的可用性)
  • 大括号初始化只是在适用的构造函数之间执行重载解析,即大括号初始化不能使用转换为类类型的运算符

这些差异导致了一些不太明显的(对我来说)情况,如

struct Intermediate {};
struct S
{
operator Intermediate() { return {}; }
operator int() { return 10; }
};
struct S1
{
S1(Intermediate) {}
};
S s;
Intermediate im1 = s; // OK
Intermediate im2 = {s}; // ill-formed
S1 s11 = s; // ill-formed
S1 s12 = {s}; // OK
// note: but brace initialization can use operator of conversion to int
int i1 = s; // OK
int i2 = {s}; // OK


5.过载分辨率的差异

  • 显式构造函数的不同处理

参见13.3.1.7通过列表初始化进行初始化

在副本列表初始化中,如果选择了explicit构造函数,则初始化格式不正确。[注意:这与其他情况(13.3.1.3、13.3.1.4),其中仅转换构造函数被考虑用于复制初始化。此限制仅适用如果此初始化是过载最终结果的一部分决议--尾注]

如果你能看到更多的差异或以某种方式纠正我的答案(包括语法错误),请这样做。


以下是C++标准当前草案的相关(但很长)摘录(我还没有找到将它们隐藏在扰流板下的方法):
所有这些都位于第8.5章Initializers 中

8.5初始化程序

  • 如果初始值设定项是一个(非括号)支撑的初始化列表对象或引用被列表初始化(8.5.4)。

  • 如果目标类型是引用类型,请参见8.5.3。

  • 如果目标类型是字符数组、char16_t数组、char32_t的数组,或wchar_t的数组,初始化器是字符串文字,见8.5.2。

  • 如果初始值设定项是(),则对象为值已初始化。

  • 否则如果目的地类型是数组,这个程序格式不正确。

  • 如果目标类型是cv合格)类别类型:

    • 如果初始化直接初始化,或者如果是复制初始化源类型的cv不合格版本与或的派生类,目的地的类,构造函数考虑。列举了适用的构造函数(13.3.1.3),以及通过过载分辨率(13.3)选择最佳调用如此选择的构造函数来初始化对象初始值设定项表达式或表达式列表作为其参数。如果没有构造函数应用,或者重载解析不明确,则初始化格式不正确。

    • [*]否则(即,对于剩余的副本初始化情况),用户定义的转换可以从源类型转换为目标类型的序列类型或(当使用转换函数时)到派生类如13.3.1.4所述列举,最好的是通过过载分辨率(13.3)选择。如果转换不能完成或不明确,则初始化格式不正确。函数selected是以初始值设定项表达式作为其参数调用的;如果该函数是一个构造函数,该调用初始化目的地类型的cv不合格版本。临时的是prvalue。调用的结果(对于constructor case)来直接初始化上面的规则,作为复制初始化。在某些情况下,允许执行通过以下方式消除此直接初始化中固有的复制将中间结果直接构造为对象初始化;参见12.2、12.8。

    • 否则,如果源类型是(可能是cv限定的)类类型,转换函数为考虑。列举了适用的转换函数(13.3.1.5),通过过载分辨率选择最佳(13.3).调用如此选择的用户定义转换进行转换初始化器表达式转换为正在初始化的对象。如果转换无法完成或不明确,初始化为不正规。

  • 否则,对象的初始值initialized是初始值设定项的值(可能已转换)表示如果必要时,将初始值设定项表达式转换为cv不合格目的地类型的版本;没有用户定义的转换考虑。如果无法完成转换,则初始化为不正规。


8.5.3参考。。。


8.5.4列表初始化

类型T的对象或引用的列表初始化定义为如下:

  • 如果T是聚合,则聚合初始化为执行(8.5.1)。

  • 否则,如果初始值设定项列表没有元素,而T是一个具有默认构造函数的类类型,即对象值已初始化。

  • 否则,如果Tstd::initializer_list<E>,prvalueinitializer_list对象为如下所述构造并用于初始化对象根据初始化类中对象的规则相同类型(8.5)。

  • [*]否则,如果T是类类型,构造函数被考虑在内。适用的构造函数为枚举,并通过过载解决方案选择最佳(13.3,13.3.1.7)。如果需要缩小转换范围(见下文)如果转换任何参数,则程序格式不正确。

  • 否则,如果初始值设定项列表具有类型为E的单个元素,并且T不是引用类型,或者其引用类型是与E相关的引用,对象或引用从该要素;如果需要缩小转换范围(见下文)将元素转换为T,则程序格式不正确。

  • 否则,如果T是引用类型,是T引用类型的临时prvalue是已初始化复制列表还是已初始化直接列表,具体取决于引用的初始化类型,并且引用已绑定这是暂时的。[注意:与往常一样,绑定将失败如果引用类型是对的左值引用,则程序格式错误非常数类型--;尾注]

  • 否则,如果初始值设定项列表没有元素,则对对象进行值初始化。

  • 否则,程序就是格式错误的。


相关文章: