【问题标题】:Any difference between copy-list-initialization and traditional copy-initialization?复制列表初始化和传统复制初始化有什么区别?
【发布时间】:2014-03-16 13:31:06
【问题描述】:

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

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

class A {...};
class B {...};

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

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

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

编辑:添加一个链接到我的一个有点相关的问题:What is the supposed behavior of copy-list-initialization in the case of an initializer with a conversion operator?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    复制初始化始终考虑复制构造函数的可用性,而复制列表初始化不考虑。

    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
    }
    

    【讨论】:

    • WTF!这很奇怪。在任何一种情况下,为什么从B 构造要关心复制ctor 是否可访问?愚蠢的语言...
    • 非常感谢。在问这个问题之前,我实际上考虑过类似的事情。我将复制构造函数设为私有,复制列表初始化无法编译,所以我认为复制列表初始化也需要复制构造函数可用,就像复制初始化一样。现在,正如您所说(我尝试过),制作复制构造函数=delete 不会影响复制列表初始化。我是 C++11 的新手,您能否详细说明构造函数为 private=delete 之间的区别?在这种情况下,差异的后果是什么?
    • @Lightness:因为T a = b; 等价于T a(T(b));(忽略MVP)。
    • @Xeo:因为它有这种荒谬的行为。
    • @goodbyeera 你能张贴你试过的东西吗? It should work 无论您将构造函数声明为私有还是已删除,都相同。两者的区别在于前者可以被其他成员函数和friends调用,而在任何上下文中调用后者都是非法的。
    【解决方案2】:

    可能,新的复制列表初始化的行为被定义为“良好”且一致,但由于向后兼容性,无法更改旧复制初始化的“奇怪”行为。
    如您所见,此子句中的列表初始化规则对于直接表单和复制表单是相同的。
    explicit 相关的差异仅在有关重载解决方案的章节中进行了描述。但是对于传统的初始化直接和拷贝形式是不一样的。
    传统初始化和大括号初始化是分开定义的,因此总是存在一些(可能是无意的)细微差别的可能性。

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

    1。已经提到的区别

    • 不允许缩小转换范围
    • 可以有多个参数
    • 如果存在,括号语法更喜欢初始化列表构造函数:

      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。存在转换运算符时引用初始化的不同行为

    大括号初始化不能使用转换为引用类型的操作符

    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),其中仅转换构造函数 被考虑用于复制初始化。此限制仅适用 如果此初始化是重载最终结果的一部分 解决。 ——结束注释 ]

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


    以下是来自current draft of the C++ standard 的相关(但很长)摘录(我还没有找到将它们隐藏在剧透下的方法):
    它们都位于第 8.5 章初始化器中

    8.5 初始化器

    • 如果初始值设定项是(非括号)braced-init-list,则 对象或引用是列表初始化的 (8.5.4)。

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

    • 如果目标类型是字符数组,char16_t 数组,一个 char32_t 的数组,或 wchar_t 的数组,初始值设定项是 字符串文字,见 8.5.2。

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

    • 否则,如果目标类型是数组, 该程序格式不正确。

    • 如果目标类型是(可能 cv-qualified) 类类型:

      • 如果初始化是 直接初始化,或者如果它是复制初始化,其中 源类型的 cv 非限定版本与相同的类,或 派生类,目的地的类,构造函数是 经过考虑的。列举了适用的构造函数(13.3.1.3),并且 通过重载决议(13.3)选择最好的一个。这 调用如此选择的构造函数来初始化对象,其中 初始化表达式或 表达式列表 作为其参数。如果不 构造函数适用,或者重载决议不明确, 初始化格式不正确。

      • [*] 否则(即,对于 剩余的复制初始化情况),用户定义的转换 可以从源类型转换为目标的序列 类型或(当使用转换函数时)到派生类 其枚举如 13.3.1.4 所述,最好的一个是 通过重载决议(13.3)选择。如果不能转换 完成或模棱两可,初始化格式不正确。功能 selected 以初始化表达式作为参数调用;如果 该函数是一个构造函数,调用初始化一个临时的 目标类型的 cv 非限定版本。临时是一个 prvalue。调用的结果(这是临时的 构造函数 case) 然后用于直接初始化,根据 上面的规则,作为目标的对象 复制初始化。在某些情况下,允许实施 消除这种直接初始化中固有的复制 将中间结果直接构造到对象中 初始化;见 12.2、12.8。

      • 否则,如果源类型是 (可能是 cv 限定的)类类型,转换函数是 经过考虑的。列举了适用的转换函数 (13.3.1.5),通过重载决议选择最好的一个 (13.3)。调用如此选择的用户自定义转换进行转换 初始化表达式到被初始化的对象中。如果 转换无法完成或不明确,初始化是 格式不正确。

    • 否则,对象的初始值被 initialized 是初始化器的(可能转换的)值 表达。将使用标准转换(第 4 条),如果 必要的,将初始化表达式转换为 cv-unqualified 目标类型的版本;没有用户定义的转换 经过考虑的。如果无法完成转换,则初始化为 格式不正确。


    8.5.3 参考文献 ...


    8.5.4 列表初始化

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

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

    • 否则,如果初始化列表没有 元素和T 是具有默认构造函数的类类型,对象 是值初始化的。

    • 否则,如果 Tstd::initializer_list&lt;E&gt;,prvalue initializer_list 对象是 如下所述构造并用于初始化对象 根据从一个类中初始化一个对象的规则 相同类型(8.5)。

    • [*] 否则,如果T 是类类型, 构造函数被考虑。适用的构造函数是 枚举并通过重载决议选择最好的一个 (13.3, 13.3.1.7)。如果需要缩小转换(见下文) 转换任何参数,程序格式错误。

    • 否则,如果初始化列表有一个 E 类型的元素并且 T 不是引用类型或其引用类型是 与E 相关的引用,对象或引用从初始化 该元素;如果需要缩小转换(见下文) 将元素转换为T,程序格式错误。

    • 否则,如果 T 是引用类型,是T 引用的类型的prvalue 临时 是复制列表初始化还是直接列表初始化,具体取决于 引用的初始化类型,并且引用被绑定 到那个临时的。 [ 注意: 像往常一样,绑定将失败,并且 如果引用类型是左值引用,则程序格式错误 非常量类型。 —尾注]

    • 否则,如果初始化列表 没有元素,对象是值初始化的。

    • 否则程序格式不正确。


    【讨论】:

    • 我试图重新格式化你的好答案并将摘录放在最后,因为我认为你的解释和总结更有用。我希望我改进了整体格式,随时恢复它。
    • 非常感谢您的详细回答。至于#5,传统的复制初始化也不适用于显式构造函数。正如另一个答案中的 cmets 中提到的那样,T a = b;T a(T(b)); 有点对称,这里的转换T(b) 是一个隐式转换,因此不适用于显式构造函数。 (虽然最后经常省略的复制构造函数调用是显式调用。但如果bT 的类型相同,那么对复制构造函数的调用仍然是隐式的。)
    • 在你的最后一个例子中,Immediate 是一个聚合,而不是一个类类型。我找到了暗示它是类类型的答案,这让我很困惑为什么复制列表初始化格式不正确。
    猜你喜欢
    • 2010-11-06
    • 2020-02-29
    • 2018-10-29
    • 2017-08-27
    • 1970-01-01
    • 2012-11-07
    相关资源
    最近更新 更多