复制初始化表单 '= {}'

Copy initialization of the form '= {}'

本文关键字:初始化 复制 表单      更新时间:2023-10-16

给定以下情况:

#include <stdio.h>
class X;
class Y
{
public:
  Y() { printf("  1n"); }             // 1
  // operator X(); // 2
};
class X
{
public:
  X(int) {}
  X(const Y& rhs) { printf("  3n"); } // 3
  X(Y&& rhs) { printf("  4n"); }      // 4
};
// Y::operator X() { printf("   operator X() - 2n"); return X{2}; }
int main()
{
  Y y{};     // Calls (1)
  printf("jn");
  X j{y};    // Calls (3)
  printf("kn");
  X k = {y}; // Calls (3)
  printf("mn");
  X m = y;   // Calls (3)
  printf("nn");
  X n(y);    // Calls (3)
  return 0;
}

到目前为止,一切都很好。现在,如果我启用转换操作员Y::operator X(),我会得到; -

  X m = y; // Calls (2)

我的理解是发生这种情况,因为(2(比(3(和(3(和因此优先。对X构造函数的调用

我的问题是,为什么X k = {y}的定义不以相同的方式改变其行为?我知道= {}从技术上讲是"复制初始化",但是在没有使用initializer_list类型的构造函数的情况下,这是否还原为"复制初始化"行为?IE-与X m = y

相同

我理解的孔在哪里?

我理解的孔在哪里?

tltldr;没有人理解初始化。

tldr;列表初始化更喜欢std::initializer_list<T>构造函数,但并不是非列表限制化的。它只能回到考虑构造函数。非列表初始化将考虑转换函数,但后备却没有。


所有初始化规则都来自[dcl.init]。因此,让我们从第一原则开始。

[dcl.init]/17.1:

  • 如果初始化器是(非父母化( braced-init list or = braced-init-list ,则对象或参考被列表initialialized。

第一个子弹点涵盖了任何列表限制。这将X x{y}X x = {y}跳到[dcl.init.list]。我们会回到这一点。另一种情况更容易。让我们看一下X x = y。我们直接调用:

[dcl.init]/17.6.3:

  • 否则(即,对于剩余的副本初始化案例(,用户定义的转换序列可以从源类型转换为目标类型,或者(当使用转换功能时(转换为派生类别的类别,如所述在[over.match.copy]中,最好的一个是通过超负荷分辨率选择的。

[over.match.copy]中的候选人是:

  • T的转换构造函数[在我们的情况下,X]是候选功能。
  • 当初始化器表达式的类型是类型" cv S"时,考虑了S及其基础类的非明确转换功能。

在这两种情况下,参数列表都有一个参数,即初始化器表达式。

这为我们提供了候选人:

X(Y const &);     // from the 1st bullet
Y::operator X();  // from the 2nd bullet

第二个相当于拥有X(Y& ),因为转换函数未得到CV的标记。这使得与转换构造函数相比,CV合格的参考较少,因此它是首选的。请注意,C 17中没有X(X&& )的调用。


现在让我们回到列表限制案例。第一个相关的子弹点是[dcl.init.list]/3.6:

否则,如果T是类型,则考虑构造函数。列举了适用的构造函数,最好通过超载分辨率([over.match],[over.match.list](选择最好的构造函数。如果需要缩小转换(见下文(才能转换任何参数,则该程序的形式不佳。

在这两种情况下,我们都将[over.match.list]定义了两相超负荷分辨率:

  • 最初,候选函数是T类的初始化列表构造函数([dcl.init.list](,参数列表由单个参数组成。
  • 如果找不到可行的初始化列表构造函数,则再次执行过载分辨率,其中候选功能是T类的所有构造函数,而参数列表由初始器列表的元素组成。

如果初始化器列表没有元素,并且t具有默认构造函数,则省略了第一阶段。在副本列表初始化中,如果选择了显式构造函数,则初始化是不形成的。

候选人是X的构造函数。X x{y}X x = {y}之间的唯一区别在于,如果后者选择了explicit构造函数,则初始化是不形成的。我们甚至没有任何explicit构造函数,因此两个是等效的。因此,我们列举了我们的构造函数:

  • X(Y const& )
  • X(X&& )通过Y::operator X()

前者是直接参考绑定,是确切的匹配。后者需要用户定义的转换。因此,在这种情况下,我们更喜欢X(Y const& )


请注意,GCC 7.1在C 1Z模式下犯错了,所以我已提交错误80943。

我的问题是,为什么定义x k = {y}不以相同的方式改变其行为?

因为,从概念上讲,= { .. }初始化,对于自动选择从括号中初始化目标初始化目标的"最佳"方法的东西,而= value也是一个初始化,但从概念上讲也将 value 转换为不同的值。转换是完全对称的:是否会查看源值以查看它是否提供了创建目标的方法,并将查看目标以查看是否提供了接受源的方法。

如果您的目标类型为struct A { int x; },则使用= { 10 }不会尝试将10转换为A(将失败(。但是它将寻求最佳(在他们眼中(初始化的形式,这在这里构成了汇总的初始化。但是,如果A不是聚合(添加构造函数(,则将调用构造函数,在您的情况下,它可以轻松地发现Y无需转换就可以接受。源和目标之间没有这样的对称性,就像使用= value表格时的转换一样。

您对转换函数的"少量"的怀疑是完全正确的。如果将转换函数作为const成员,则将变得模棱两可。