不可移动对象数组初始化:GCC编译异常与标准合规性探究
GCC与Clang/MSVC在类内原生数组初始化的行为差异解析
首先来看你给出的代码示例:
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}} {} // GCC编译失败,但Clang和MSVC可编译 private: Test test_[2]; }; //---------------- int main() { B b; /*(2)*/ Test test[2] = {{1, 2}, {3, 4}}; // GCC、Clang、MSVC均可成功编译 }
问题1:类内数组与局部数组初始化的行为差异,谁的行为符合标准?
这本质上是编译器对C++标准中聚合初始化规则的实现差异,咱们从标准和编译器实现两方面分析:
- 对于
/*2*/行的局部数组初始化:这是标准的聚合初始化场景,根据C++11及以后的标准,聚合初始化会直接在数组元素的内存地址上原地调用构造函数,完全不需要创建临时对象,也不会触发任何移动/复制构造函数的检查——因为根本不需要移动或复制。这也是三个编译器都能通过编译的原因,完全符合标准。 - 对于
/*1*/行的类内成员数组初始化:早期C11标准对类成员数组的聚合初始化规则描述不够明确,导致GCC的旧版本采用了错误的实现逻辑:它认为需要先创建临时的Test对象,再将这些临时对象移动到类内数组的元素位置,因此会强制检查移动构造函数是否可用(而你的Test类已经删除了移动构造)。但后续的C标准(尤其是C++17及以后)明确了:类成员数组的初始化应该和局部数组遵循完全一致的规则——原地构造,无需临时对象。
结论:从标准角度,Clang和MSVC的行为是正确的,GCC的报错是一个历史遗留bug(该问题在GCC 10及以上版本已经被修复,你可以升级编译器验证)。
问题2:替换为std::array后GCC为何不报错?
当把原生数组换成std::array<Test, 2>时,核心在于std::array的类型属性:
std::array是标准定义的聚合类型,它的初始化属于聚合初始化的范畴。在类的成员初始化列表中初始化std::array时,编译器会递归地对其内部的元素进行原地构造——直接调用Test(int, int)构造函数,完全不需要创建临时对象,自然也不会触发移动/复制构造的检查。- GCC对
std::array的聚合初始化逻辑一直是正确的,因为std::array的结构更清晰,编译器能明确识别这是聚合类型的初始化,不会走错误的临时对象创建路径,因此三个编译器都能顺利编译。
内容的提问来源于stack exchange,提问作者Taras




