复制初始化形式 '= {}'

Copy initialization of the form '= {}'

鉴于以下情况:

#include <stdio.h>

class X;

class Y
{
public:
  Y() { printf("  1\n"); }             // 1
  // operator X(); // 2
};

class X
{
public:
  X(int) {}
  X(const Y& rhs) { printf("  3\n"); } // 3
  X(Y&& rhs) { printf("  4\n"); }      // 4
};

// Y::operator X() { printf("   operator X() - 2\n"); return X{2}; }

int main()
{
  Y y{};     // Calls (1)

  printf("j\n");
  X j{y};    // Calls (3)
  printf("k\n");
  X k = {y}; // Calls (3)
  printf("m\n");
  X m = y;   // Calls (3)
  printf("n\n");
  X n(y);    // Calls (3)

  return 0;
}

到目前为止,还不错。现在,如果我启用转换运算符 Y::operator X(),我会得到这个;-

  X m = y; // Calls (2)

我的理解是,发生这种情况是因为 (2) 比 (3) 'less const' 并且 因此首选。省略了对 X 构造函数的调用

我的问题是,为什么定义 X k = {y} 没有以同样的方式改变它的行为?我知道 = {} 在技术上是 'list copy initialization',但是在没有采用 initializer_list 类型的构造函数的情况下,这不会恢复到 'copy initialization' 行为吗?即 - 与 X m = y

相同

我理解的漏洞在哪里?

Where is the hole in my understanding?

tltldr;没人懂初始化。

tldr;列表初始化更喜欢 std::initializer_list<T> 构造函数,但它不会回退到非列表初始化。它只会退回到考虑构造函数。非列表初始化会考虑转换函数,但回退不会。


所有初始化规则均来自[dcl.init]。所以让我们从第一原则开始吧。

[dcl.init]/17.1:

  • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized.

第一个要点涵盖所有列表初始化。这会跳转 X x{y}X x = {y}[dcl.init.list]。我们会回到那个。另一种情况更容易。让我们看看X x = y。我们直接向下调用:

[dcl.init]/17.6.3:

  • Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution.

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

  • The converting constructors of T [in our case, X] are candidate functions.
  • When the type of the initializer expression is a class type “cv S”, the non-explicit conversion functions of S and its base classes are considered.

In both cases, the argument list has one argument, which is the initializer expression.

这给了我们候选人:

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:

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

这两种情况都将我们带到 [over.match.list],它定义了两相过载解决方案:

  • Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.
  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

If the initializer list has no elements and T has a default constructor, the first phase is omitted. In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

候选人是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 模式下会出错,因此我已提交 bug 80943

My question is, why doesn't the definition X k = {y} change its behavior in the same way?

因为,从概念上讲,= { .. } 是一个 初始化,用于自动选择 "best" 方式来初始化目标 从大括号开始,虽然= value也是一个初始化,但在概念上也是到不同值的转换。转换是完全对称的:如果将查看源值以查看它是否提供创建目标的方法,并将查看目标以查看它是否提供接受源的方法。

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

你对转换函数"less const"的怀疑是完全正确的。如果将转换函数设为 const 成员,那么它将变得不明确。