初始化不可移动对象数组:为什么这样的代码无法在 GCC 上编译?

Initialization of an array of non-moveable objects: why does such code fail to compile on GCC?

本文关键字:代码 GCC 编译 对象 可移动 数组 为什么 初始化      更新时间:2023-10-16

下面是一个代码示例,其中Test是一个不可复制不可移动的类,具有一些virtual成员和一个用户定义的构造函数,B是一个包含Test对象的原始(C 样式(数组的类:

class Test
{
public:
Test() = delete;
Test(const Test&) = delete;
Test(Test&&) = delete;
Test& operator=(const Test&) = delete;
Test& operator=(Test&&) = delete;
Test(int a, int b) : a_(a), b_(b) {}
virtual ~Test() {}
int a_;
int b_;
};
//----------------
class B
{
public:
/*(1)*/ B() : test_{{1, 2}, {3, 4}} {} // Does not compile on GCC, but compiles on Clang and MSVC
private:
Test test_[2];
};
//---------------- 
int main()
{
B b;
/*(2)*/ Test test[2] = {{1, 2}, {3, 4}}; // Successfully compiles on GCC, Clang and MSVC
}

我想使用支撑初始化语法(第/*1*/行(初始化B的内部数组test_,以便就地构造两个Test对象中的每一个,而无需创建一个临时然后移动它。

在 Clang 和 MSVC 上,此代码编译时没有警告。

但是GCC的行为让我感到困惑:它无法编译行/*1*/,同时成功编译行/*2*/,我使用相同的语法来初始化本地数组。然而,对于编译第一行,它仍然需要类Test的已删除移动构造函数。

问题是,为什么?C++标准是否明确定义了这些行是否应该编译/*1*//*2*/?如果是这样,从标准的角度来看,哪个编译器是正确的?这种不一致的行为是否可以称为GCC错误,或者Clang和MSVC是否忽略了它们应该执行的一些检查?

我可以理解 GCC 可能需要一个移动构造函数才能从内部大括号 ({1, 2}( 创建一个临时Test对象,然后将该对象移动到数组中。因此编译错误。但如果是这样,为什么它没有因为同样的原因而/*(2)*/失败呢?在这个例子中,这是我最困惑的事情。


顺便说一下,这里有一个有趣的观察:如果我用std::array<Test, 2>(而不是"C 样式"数组(替换test_的定义,并将构造函数初始化列表中的代码替换为test_{{{1, 2}, {3, 4}}},一切都开始在所有提到的三个编译器上成功编译。

我也不清楚为什么 GCC 在这种情况下在任何一行上都没有失败,而"原始"数组却失败了。

谁能解释一下?

我认为初始化没有问题,所以我认为这是一个 GCC 错误。


所涉及的初始化是列表初始化,所以我们咨询 [dcl.init.list]/3:

定义了T类型的对象或引用的列表初始化 如下:

  • [...]

  • (3.3( 否则,如果T是一个聚合,则聚合初始化为 执行。

  • [...]

(数组是一个聚合。 现在我们转到 [dcl.init.aggr]/3:

当聚合由指定的初始值设定项列表初始化时 在 [dcl.init.list] 中,初始值设定项列表的元素被视为 聚合元素的初始值设定项,按顺序排列。每 元素从相应的初始值设定项子句如果初始值设定项子句是表达式,并且需要缩小转换才能转换表达式,则 程序格式不正确。

因此,对于这两个元素中的任何一个,我们实际上是在做Test a = {1, 2},这是有效的,因为Test(int, int)不是显式的。 因此,初始化的格式正确,编译器应接受。