聚合体的列表初始化:什么时候会调用复制构造函数?

7
考虑以下代码:
struct A {
  int x;
};

int main() {
  A a;
  A b{a};
}

这个程序符合C++11标准吗?在我的N3797副本中,它说
8.5.4 列表初始化[dcl.init.list]
3: 对于类型T的对象或引用,列表初始化定义如下:
- 如果T是一个聚合体,则执行聚合初始化(8.5.1)。
- 否则,如果T是std::initializer_list<E>的特例...
- 否则,如果T是类类型,则考虑构造函数。 枚举适用的构造函数,并使用重载解析选择最佳构造函数。 如果需要将任何类型进行缩小转换,则程序是非法的。
- 否则,如果初始化列表具有类型为E的单个元素,并且T不是引用类型或与E相关联,则从该元素初始化对象或引用; 如果需要将该元素转换为T进行缩小转换,则程序是非法的。
- 否则,如果T是引用类型,则将指向T引用的pr-value临时对象进行复制列表初始化或直接列表初始化,具体取决于引用的初始化方式,并将引用绑定到该临时对象上。
- 否则,如果初始化列表没有元素,则对对象进行值初始化。
- 否则,程序是非法的。

这个例子的重点是,类型是一个聚合体,但列表初始化应该调用复制构造函数。在C++11标准下,在gcc 4.8gcc 4.9上失败:

main.cpp: In functionint main()’:
main.cpp:7:8: error: cannot convert ‘Atointin initialization
   A b{a};
        ^

并且提示“无法将A转换为int”或类似的错误,因为聚合初始化失败。在gcc 5.4上,它在C++11标准下工作正常。
在clang上,您会得到类似的错误,例如clang-3.5、3.6,并且在clang-3.7开始工作。
我知道在C++14标准下它是规范的,并且在一个缺陷报告中提到了这一点here
然而,我不明白为什么这被认为是标准中的缺陷。
当标准写道,
“如果X,则执行foo初始化。否则,如果Y,则执行bar初始化……否则,程序不符合规范。”
这难道不意味着,如果X成立,但无法执行foo初始化,那么我们应该检查Y是否成立,然后尝试bar初始化吗?
这将使示例工作,因为当聚合初始化失败时,我们不匹配std :: initializer_list ,下一个条件是“ T 是类类型”,然后我们考虑构造函数。

请注意,这似乎是在这个修改后的示例中如何工作的。

struct A {
  int x;
};

int main() {
  A a;
  const A & ref;
  A b{ref};
}

所有编译器都会按照C++11和C++14标准处理这个例子。但是,似乎CWG缺陷记录中修改的措辞并不适用于此情况。它的原话是:“如果T是类类型,并且初始化列表有一个类型为cv T或从T派生的类类型的单个元素,则对象从该元素初始化。”

http://open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1467

但在第二个代码示例中,初始化列表在技术上包含const T&。所以我不明白,除非聚合初始化失败后,我们应该尝试构造函数,否则它怎么能工作。

我错了吗?聚合初始化失败后不应该尝试构造函数吗?

这里是一个相关的例子:

#include <iostream>

struct B {
  int x;

  operator int() const { return 2; }
};

int main() {
  B b{1};
  B c{b};
  std::cout << c.x << std::endl;
}

clang-3.6, gcc-4.8, gcc-4.9 中,它会打印 2,而在 clang-3.7, gcc-5.0 中,它会打印 1。假设我错了,并且在 C++11 标准中,聚合体的列表初始化应该是聚合初始化而不是其他任何内容,直到缺陷报告中的新措辞被引入,那么当我在新编译器上选择 -std=c++11 时,这种情况是否是一个错误?
2个回答

4

当标准写道:

“如果 X 成立,则执行 foo 初始化。否则,如果 Y 成立,则执行 bar 初始化。……否则,程序不符合规范。”

这是否意味着,如果 X 成立,但无法执行 foo 初始化,那么我们应该检查是否 Y 成立,然后尝试 bar 初始化?

不,不是这样的。可以将其看作实际代码:

T *p = ...;
if(p)
{
  p->Something();
}
else
{ ... }

p 不是 NULL,但这并不意味着它是一个有效的指针。如果 p 指向一个被销毁的对象,那么 p->Something() 失败将不会导致你跳到 else。你有机会在条件语句中保护调用。

因此,你会得到未定义的行为。

同样的道理也适用于这里。如果 X,则执行 A。这并不意味着如果 A 失败会发生什么事情;它只是告诉你去做。如果无法完成... 那就糟糕了。


那么你认为在我的struct B示例中,当使用std=c++11时,gcc-5应该与gcc-4.8gcc-4.9表现相同吗? - Chris Beck
@ChrisBeck:那是一个不同的问题。那是一个关于缺陷修复是否应该追溯应用于现有标准的问题。一般来说,答案是肯定的。这就是缺陷报告的作用;它是标准中的一个错误。错误应该被修复,而且你不应该必须说你正在使用下一个版本才能得到它们,对吧? - Nicol Bolas
我的意思是,标准实现中的错误和标准措辞中的缺陷之间存在差异。前者应该通过仅增加补丁级别的发布来解决。但是,如果我说 std=c++11,我通常认为他们正在尝试实现 N3797。否则,除非我阅读所有标准,否则我无法真正知道编译器将尝试做什么。或者我必须告诉人们:“您不能在版本3.6之后使用clang,否则它将编译但初始化不会按预期工作,您将看到1而不是2”。 - Chris Beck
我明白了。所以在缺陷报告页面上,它说:“DR、accepted、DRWP 和 WP 状态的问题不属于 C++ 国际标准的一部分。它们仅供信息目的,作为委员会意图的指示。”当我要求编译器使用 --std=c++11 时,这意味着 C++11 标准文档中描述的 C++,再加上任何澄清标准委员会意图的缺陷报告?或者类似这样的东西? - Chris Beck
我猜这种改变极不可能会破坏为c++11设计的程序——我的例子相当人为,一个聚合体不能包含自身的副本,所以 A b{a}; 只会在旧编译器中失败,除非涉及这些古怪的隐式转换。也许如果聚合体包含对自身的引用之类的东西,但那也相当人为。 - Chris Beck
显示剩余2条评论

3
当标准写道:“如果X,则进行foo初始化。否则,如果Y,则进行bar初始化...”时,这是否意味着如果X成立,但无法执行foo初始化,那么我们应该检查Y是否成立,然后尝试bar初始化?
不是的。如果X成立,我们将执行foo初始化。如果失败,则程序将不合法。

那么你对 struct B 的例子有什么看法?你认为 gccclang 是否使用了一些 C++14 的特性,而这些特性在 C++11 标准中不应该存在? - Chris Beck
@ChrisBeck:我认为他们已经确定这是C++11标准中的明显缺陷,并且已经实现了修复版本。我猜想这从未得到“解决”的唯一原因是他们开始更快地发布C++版本,因此不是在旧标准中修复缺陷,而是在最新版本中修复它。 - Martin Bonner supports Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接