该C++代码是GCC编译错误还是存在未定义行为?
首先看问题中的代码:
#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; }
这段代码在GCC 13及更早版本中返回0,在MSVC、Clang等编译器中返回66,本质是因为代码存在未定义行为,而非某款编译器存在编译错误。
未定义行为的原因
当基类DefaultMsgImpl<Msg>的构造函数执行时,派生类Msg的生命周期尚未开始。根据C++标准,对象的生命周期从其初始化完成(包括所有子对象的初始化)时才启动,在基类构造阶段,派生类的成员还未被初始化,此时操作派生类对象的内存属于访问尚未进入生命周期的对象。
引用C++标准相关条款
根据C++标准[basic.life]/1:
一个类型为T的对象的生命周期开始于:(1) 获得了适合T类型的对齐和大小的存储,且(2) 其初始化(如果有的话)完成,包括所有子对象的初始化。同时[class.cdtor]/1规定:
对于具有非平凡构造函数的对象,在构造函数开始执行前引用该对象的任何非静态成员或基类,会导致未定义行为。类似地,如果对象是具有非平凡析构函数的类类型,且析构函数已开始执行,在构造函数完成执行后引用该对象的任何非静态成员或基类,也会导致未定义行为。
代码问题分析
基类构造函数里的memset(this, 0, sizeof(T))中,T是Msg,所以sizeof(T)包含了基类和派生类成员num的内存大小。此时Msg对象的生命周期还没开始,num所在的内存虽然已分配,但直接用memset覆盖这块区域,属于修改尚未完成初始化的派生类成员,完全符合上述标准中未定义行为的范畴。
编译器表现差异的本质
未定义行为的核心是C++标准不对程序的行为做任何强制规定,所以不同编译器(甚至同一编译器的不同优化级别)可能产生完全不同的结果:GCC中memset覆盖了后续聚合初始化的num值,返回0;而其他编译器可能因为初始化顺序或优化策略,让聚合初始化的num值覆盖了memset的结果,返回66。这些都是未定义行为的合理表现,没有哪款编译器的行为是"错误"的。
内容的提问来源于stack exchange,提问作者John Zwinck

