是 GCC 错误编译了这段代码,还是 UB 错误编译了这段代码?

Joh*_*nck 22 c++ gcc language-lawyer

考虑这段代码:

#include <cstring>

template<typename T>
struct DefaultMsgImpl
{
    DefaultMsgImpl() { memset(this, 0, sizeof(T)); }
};

struct Msg : DefaultMsgImpl<Msg>
{
    int num;
};

int f()
{
    Msg msg{.num = 66};
    return msg.num;
}
Run Code Online (Sandbox Code Playgroud)

对于 GCC 13(及更早版本),f()返回 0,但对于所有其他编译器(MSVC、Clang、ICC、ICX...),f()返回 66。

上面的代码是有效的C++(在这种情况下,GCC 似乎对其进行了错误编译),还是未定义的行为(例如,因为 memset 在 的生命周期开始Msg之前接触了内存)?T如果是 UB,我希望得到引用 C++ 标准的答案。

演示: https: //godbolt.org/z/1qf9dv8cG

Swi*_*Pie 3

无论您设置什么方法,在这种情况下发生的情况都是 UB Msg。访问被派生类重写的纯虚方法或从基类的构造函数访问派生类的非静态成员是一个 UB。

正式类型TakaMsg尚未构建。因此,虽然在 s 初始化后从方法中执行类似的操作Msg(相对)很好并且是 CRTP 模式的核心,但从构造函数中执行此操作则不然。

请注意,扩展初始化器{.num = 66};是 C99 功能,但在某些 C++ 编译器中(或者在双语编译器的 C++ 模式下)中具有初步的历史支持,这使得该代码在 ISO C++20 之前形式上不正确。在 -pedantic 模式下,GCC 会对此发出警告。扩展与构造函数混合时究竟应该如何工作,从未被定义过。事实上,GCC 首先执行初始化,然后运行构造函数的主体。

指定初始化是聚合初始化。在 ISO C++20 中,仅当不存在继承或用户定义的构造函数时才允许这样做,但此处的情况并非如此。例如,在 GCC 版本的 GNU-C++17 上,这似乎不是必需的。

根据 ISO 标准,非联合聚合的元素可以默认初始化或显式初始化。否则代码格式错误,无需诊断。(9.4.5 聚合初始化器 n 4868)。您可以通过使用 来规避编译器可能但不必要的抱怨memset

如果存在某种先有鸡还是先有蛋的问题,该模式可以通过基类进行扩展,该基类将保存 CRTP 基所需的所有非静态成员和定义:

template <typename T>
struct MsgTrait {};

template<typename T>
struct DefaultMsgImpl : MsgTrait<T>
{
    DefaultMsgImpl(int num) { this->num = num; }
};

// Msg
struct Msg;
template <>
struct MsgTrait<Msg> {
    int num;
    //something else?
};    

struct Msg : DefaultMsgImpl<Msg>
{
    Msg(int MsgNumber) : DefaultMsgImpl(MsgNumber) {}
};

int f()
{
    // Msg msg{.num = 66}; cannot do that. And Msg cannot be trivially constructed
    Msg msg{66};
    return msg.num;
}
Run Code Online (Sandbox Code Playgroud)

MsgTrait<Msg>将首先构造和初始化,并且被认为是完整类型,及其所有嵌套类型(如果存在),只要在尝试实例化之前定义了其专门化DefaultMsgImpl<Msg>

  • 小修正:使用虚拟,只要它们不是纯虚拟,当前正在构建的类型就可以。只需避免显式访问尚未构造的同级或包含对象即可。这并不是说它改变了所讨论的场景。 (2认同)