【问题标题】:Copy initialization of the form '= {}'复制“= {}”形式的初始化
【发布时间】:2017-11-02 13:08:24
【问题描述】:

鉴于以下情况:

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

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

我的理解的漏洞在哪里?

【问题讨论】:

  • 对不起 - 我搞砸了最小化代码 - 已更正(希望如此)
  • 使用了 Exaxt 编译器?我知道在某些编译器中与此相邻的代码不一致。
  • 改进了示例。我已经在 11、14 和 17 模式下使用 cppreference.com(clang 3.8)上的编译器工具进行了尝试。结果是一样的
  • 感谢大家的帮助和全面的回答。我想我明白现在发生了什么。但我必须同意 Barry 的观点——C++ 初始化太疯狂了! :-)

标签: c++ c++11 c++14 list-initialization copy-initialization


【解决方案1】:

我的理解的漏洞在哪里?

tltldr;没人懂初始化。

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


所有的初始化规则都来自[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] 的转换构造函数是候选函数。
  • 当初始化表达式的类型是类类型“cvS”时,考虑S及其基类的非显式转换函数。

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

这给了我们候选人:

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

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


现在让我们回到列表初始化案例。第一个相关的要点是[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&amp; )
  • X(X&amp;&amp; ) 通过Y::operator X()

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


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

【讨论】:

  • 您没有被“告知考虑转换函数”,因为在确定 X 的复制/移动构造函数是否是可行的候选者时会考虑它们。他们只是无法与完全匹配的参考绑定竞争。
  • @T.C.所以X x{y} 会认为X(X&amp;&amp;) 通过Y::operator X 是一个可行的候选人,只是一个不太可行的候选人?或者换句话说,我提交的 gcc 错误是一个错误,但不是因为我描述的原因?
  • 差不多,是的。
  • 您报告的错误也发生在T t(s)
【解决方案2】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 2015-05-18
    • 2013-01-16
    • 1970-01-01
    • 1970-01-01
    • 2013-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    相关资源
    最近更新 更多