14

鉴于以下情况:

#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)“更少”,因此是首选。X省略了对构造函数的调用

我的问题是,为什么定义没有X k = {y}以同样的方式改变它的行为?我知道这= {}在技术上是“列表复制初始化”,但是在没有构造函数采用initializer_list类型的情况下,这不会恢复为“复制初始化”行为吗?ie - 与 for 相同X m = y

我的理解的漏洞在哪里?

4

2 回答 2

7

我的理解的漏洞在哪里?

tltldr; 没有人了解初始化。

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


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

[dcl.init]/17.1

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

第一个要点涵盖任何列表初始化。这会跳转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函数是候选函数。
  • 当初始化表达式的类型是类类型“<em>cv ”时,考​​虑其及其基类S的非显式转换函数。S

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

这给了我们候选人:

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

第二个等效于具有 a X(Y& ),因为转换函数不是 cv 限定的。这使得 cv 限定的引用比转换构造函数少,所以它是首选。X(X&& )注意,在 C++17 中没有调用here。


现在让我们回到列表初始化案例。第一个相关的要点是[dcl.init.list]/3.6

否则,如果T是类类型,则考虑构造函数。枚举适用的构造函数,并通过重载决议([over.match]、[over.match.list])选择最佳构造函数。如果需要缩小转换(见下文)来转换任何参数,则程序格式错误。

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

  • 最初,候选函数是类 T 的初始化列表构造函数([dcl.init.list]),参数列表由初始化列表作为单个参数组成。
  • 如果没有找到可行的初始化列表构造函数,则再次执行重载决议,其中候选函数是类 T 的所有构造函数,参数列表由初始化列表的元素组成。

如果初始化列表没有元素并且 T 有默认构造函数,则省略第一阶段。在复制列表初始化中,如果选择了显式构造函数,则初始化格式错误。

候选人是 的构造函数XX x{y}和之间的唯一区别X x = {y}是,如果后者选择explicit构造函数,则初始化格式不正确。我们甚至没有任何explicit构造函数,所以两者是等价的。因此,我们枚举我们的构造函数:

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

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


请注意,gcc 7.1 在 C++1z 模式下会出错,所以我提交了错误 80943

于 2017-06-01T14:48:32.027 回答
0

我的问题是,为什么定义 X k = {y} 没有以同样的方式改变它的行为?

因为,从概念上讲, a= { .. }是一种初始化,它自动选择从大括号中初始化目标的“最佳”方式,同时= value也是一种初始化,但在概念上也是将转换为不同的值。转换是完全对称的:如果将查看源值以查看它是否提供了创建目标的方法,并将查看目标以查看它是否提供了接受源的方法。

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

您对转换函数的“less const”的怀疑是完全正确的。如果您将转换函数设为 const 成员,那么它将变得模棱两可。

于 2017-06-01T20:06:09.150 回答