将C++结构体别名化为其首个成员是否违反strict aliasing规则?
先来看你给出的示例代码:
struct S { int x; }; int func() { S s{2}; return (int &)s; // Equivalent to *reinterpret_cast<int *>(&s) }
你提到这种写法很常见,而且标准确实保证这个单成员标准布局结构体没有前置填充,这点没错。但对照C++17 [basic.lval]/11的strict aliasing规则,看起来好像找不到直接对应的条款?我们先再明确一下这条规则的内容:
若程序尝试通过以下类型之外的glvalue访问对象的存储值,则行为未定义:
- (11.1) 对象的dynamic type,
- (11.2) 对象dynamic type的cv限定版本,
- (11.3) 与对象dynamic type相似的类型(定义见7.5),
- (11.4) 与对象dynamic type对应的有符号或无符号类型,
- (11.5) 与对象dynamic type的cv限定版本对应的有符号或无符号类型,
- (11.6) 聚合或联合类型,其元素或非静态数据成员包含上述类型之一(递归包含子聚合或所含联合的元素或非静态数据成员),
- (11.7) 对象dynamic type的(可能带cv限定的)基类类型,
- (11.8) char、unsigned char或std::byte类型。
你已经注意到,这里的条款都是针对执行访问的glvalue的类型,而这段代码里glvalue的类型是int,所以11.6(要求glvalue是聚合/联合类型)确实不适用。那这段代码到底合不合法?
答案是:这段代码的行为是完全定义良好的,它符合条款(11.1)的规定。
为什么这么说?关键在于标准对标准布局结构体的特殊规定:C++17 [class.mem]/20明确指出,指向标准布局结构体对象的指针(经过适当的reinterpret_cast转换后)会指向它的第一个非静态数据成员,反之亦然。对于struct S这种只有一个int成员的标准布局结构体来说,&s的地址和&s.x的地址是完全相同的,而且整个结构体的存储没有额外填充,所以reinterpret_cast<int*>(&s)本质上等价于&s.x。
当你用int&去绑定s时,实际访问的是s.x这个int类型的对象——而此时glvalue的类型是int,正好是被访问对象(s.x)的dynamic type,完全符合(11.1)的要求。
换个角度理解:虽然代码写法上是直接cast整个结构体对象,但由于标准布局的特性,这个操作实际上等同于访问结构体的唯一成员,而不是直接访问结构体对象本身,所以并没有违反strict aliasing规则。
内容的提问来源于stack exchange,提问作者M.M

