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

Tar*_*ras 5 c++ arrays gcc language-lawyer

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

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
}
Run Code Online (Sandbox Code Playgroud)

我想使用大括号的初始化语法(第行)来初始化B的内部数组,这样就可以就地构造两个对象,而无需创建一个临时对象然后再移动它。test_/*1*/Test

在Clang和MSVC上,此代码无需警告即可编译。

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

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

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


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

我还不清楚为什么在这种情况下GCC 在任何情况下都不会失败,而“原始”数组会失败。

谁能解释一下?

L. *_* F. 4

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


涉及到的初始化是列表初始化,所以我们参考[dcl.init.list]/3

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

  • [...]

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

  • [...]

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

当聚合由 [dcl.init.list] 中指定的初始值设定项列表初始化时,初始值设定项列表的元素将按顺序作为聚合元素的初始值设定项。每个元素都是从相应的初始化子句复制初始化的 。如果初始化子句是一个表达式并且需要缩小转换来转换该表达式,则该程序是格式错误的。

因此,对于这两个元素中的任何一个,我们实际上都在执行Test a = {1, 2},这是有效的,因为Test(int, int)它并不明确。因此,初始化是格式良好的并且应该被编译器接受。