You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

不可移动对象数组初始化: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

火山引擎 最新活动