当前位置: 首页 > news >正文

CppCon 2014 学习:The Perils of Strict Aliasing

你引用的是 C++ 标准中关于**对象存储访问规则(aliasing rules)**的条款,具体是 [basic.lval]/10(也叫做“strict aliasing rule”的核心内容)。我们来逐句解读并深入理解这个规定。

条款原文简化回顾(§3.10.10)

如果程序试图通过一个 glvalue(广义左值) 来访问某个对象的存储值,但这个 glvalue 的类型不是以下列表中的任何一种,那么这种行为是未定义的(undefined behavior, UB)

哪些类型是允许的(可以“安全 alias”)

假设对象的实际类型(dynamic type)是 int,下面这些是允许访问的 glvalue 类型:

类型示例原因
① 对象自身类型int同类型访问
cv 限定版本const int, volatile int加了限定词也行
③ 类似类型(similar type)指大小和布局相同的类型(定义在§4.4)比如 typedef int myint;
④ 对应的 signed/unsigned 类型unsigned intintC++ 特例
⑤ 带 cv 的 signed/unsigned 类型const unsigned int同上
⑥ 包含该类型的聚合或联合体如 union 中有 int 成员通过 union 成员访问
⑦ 基类类型BaseDerived 的基类多态访问
charunsigned char万能 alias 类型:可用于原始字节访问

举个未定义行为的例子(违反 aliasing)

float f = 3.14f;
int* ip = (int*)&f;  //  undefined behavior: accessing float as int
std::cout << *ip;

虽然很多编译器允许这么做,但从标准角度看,这是 UB,因为 int 不是 float 的允许 alias 类型。

合法的方式:使用 memcpystd::bit_cast(C++20)

float f = 3.14f;
int i;
std::memcpy(&i, &f, sizeof(i));  //  合法,字节复制,不触发 UB

或(C++20)

int i = std::bit_cast<int>(f);  //  安全合法的类型转换

为什么要有 strict aliasing rule?

主要目的是:

  • 允许编译器优化:当编译器知道两个不同类型的指针不可能指向同一内存,就能大胆优化代码。
  • 提高执行效率:尤其在现代 CPU 的流水线和缓存机制下。

附注:char 类型的特权

charunsigned char 被允许访问任何类型对象的字节内容(即“byte-wise aliasing”)。

所以这合法:

int x = 42;
unsigned char* p = reinterpret_cast<unsigned char*>(&x);
std::cout << (int)p[0]; //  合法,按字节读写

总结

内容说明
glvalue 访问对象只有特定类型访问是合法的
其他类型访问是 undefined behavior(UB)
例外char, unsigned char, union 成员等
目的提供优化空间、防止隐式错误
推荐方式使用 std::bit_castmemcpy、类型安全访问等方式避免 aliasing 问题

详细讲解你提到的代码是否是合法的 C++ 行为。

代码分析

uint32_t swaphalves(uint32_t a) 
{ auto ptr = reinterpret_cast<uint16_t*>(&a); std::swap(ptr[0], ptr[1]); return a; 
}

它的作用:

这段代码的目的是将一个 uint32_t 类型整数的高 16 位和低 16 位互换
举个例子:

a = 0x12345678;  // 十六进制表示
ptr[0] = 0x5678; // 低 16 位
ptr[1] = 0x1234; // 高 16 位

交换后:

a = 0x56781234;

是否是合法的行为

不完全合法。原因是:

违反了 C++ 的 strict aliasing(严格别名规则)

标准规定:你只能通过某些特定类型的指针或引用访问对象的内存内容,否则会是 未定义行为(undefined behavior)
对于一个 uint32_t 类型的对象,你只能通过以下类型访问它:

  • uint32_tconst uint32_t
  • int32_t(有符号/无符号的对应类型)
  • charunsigned char(用于按字节访问)
  • 包含它的 union、相同布局的结构体等
合法的访问方式:
uint32_t* p = &a;
非法的访问方式:
uint16_t* p = reinterpret_cast<uint16_t*>(&a);  //  违反 aliasing 规则

虽然大多数编译器(如 GCC、Clang)在某些平台(如 x86)上运行时没问题,但这是标准层面上的未定义行为,可能会在优化或不同编译器设置下出问题。

正确写法:使用位操作(推荐)

标准且高效的做法是使用位运算实现“高低 16 位交换”:

uint32_t swaphalves(uint32_t a) {return (a << 16) | (a >> 16);
}

为什么这更好:

  • 没有类型别名问题
  • 没有指针转换
  • 编译器会自动优化成一条交换指令(如 ror
  • 完全标准、安全、跨平台

总结

方法合法性推荐程度说明
reinterpret_cast<uint16_t*>(&a)不合法(UB)不推荐违反别名规则
std::memcpystd::bit_cast(C++20)合法推荐安全地重解释类型
位运算:`(a << 16)(a >> 16)`合法强烈推荐快速、清晰、安全
如果你是做底层驱动、嵌入式或特定平台代码,有时可能“知道它能用”,但建议仍然尽量写成标准允许的形式,避免难以排查的 bug。
需要我用 std::memcpybit_cast 版本实现也可以告诉我。

你这个例子用的是 union(联合体) 来做高低 16 位交换,属于 C++ 中常见的类型重解释技巧。我们来看它是否合法,有没有风险。

代码回顾

uint32_t swaphalves_union_ptr(uint32_t a) 
{ union u { uint32_t dw; int16_t w[2]; }; auto u_ptr = reinterpret_cast<u*>(&a); std::swap(u_ptr->w[0], u_ptr->w[1]); return u_ptr->dw; 
}

关键点分析

1. 访问对象内存时的 aliasing 规则

标准中提到:

通过包含该类型成员的 union访问该对象,是允许的。

u 联合体包含一个 uint32_t 成员 dw 和一个 int16_t[2] 数组成员 w
你传入的是一个 uint32_t a,取它地址强制转换成 u*,然后通过 w 访问。

2. 这是不是符合 strict aliasing 规则?

  • 联合体类型的成员访问被认为是允许的“别名”访问(aliasing)。
  • 但这里的关键点是:
    • 你的 a一个独立的 uint32_t 变量,而不是原本就是 u 类型的对象。
    • 你只是强制把 uint32_t* 指针 reinterpret_cast 成了 u*

3. 是否安全?

根据 C++ 标准(尤其是严格的解释):

  • 不能通过 u* 去访问一个原本是 uint32_t 对象的内存,除非该对象本身就是 u 类型。
  • 这仍然是未定义行为(UB)
    简而言之:

只有当对象本身是 union 类型,才能通过其成员访问内存。

仅靠 reinterpret_cast 转换指针来访问不同的 union 成员并不保证合法。

4. 运行时效果

在大多数编译器(GCC、Clang、MSVC)和平台上,这个代码通常能正常工作,因为底层内存布局是兼容的。
但是从标准合规角度讲,存在未定义行为风险

5. 正确且标准的做法

  • 避免reinterpret_cast 来强制转指针。
  • 如果要用 union 来做类型重解释,应该直接定义变量为 union 类型:
union u 
{ uint32_t dw; int16_t w[2]; 
};
uint32_t swaphalves_union(uint32_t a) {u value;value.dw = a;std::swap(value.w[0], value.w[1]);return value.dw;
}

这才是标准认可且安全的访问方式

6. 更推荐的移植性写法

直接用位操作:

uint32_t swaphalves(uint32_t a) {return (a << 16) | (a >> 16);
}

总结表

方案是否标准合法说明
reinterpret_cast<uint16_t*>否(UB)违反 strict aliasing
union 指针 reinterpret_cast否(UB)指针转换访问非 union 对象成员
union 变量直接访问对 union 变量访问其成员合法
位运算最简单且可移植的实现

这段代码理解和合法性分析

uint32_t swaphalves_union(uint32_t a) 
{ union { uint32_t dw; int16_t w[2]; }; dw = a; std::swap(w[0], w[1]); return dw; 
}

1. 匿名 union 作为局部变量

这里你定义了一个匿名 union作为局部变量(未命名变量名),它有两个成员:

  • dw —— 32 位无符号整数
  • w[2] —— 两个 16 位整数组成的数组
    匿名 union 的所有成员都可以直接访问,就像它们是函数体中的普通变量。

2. 使用步骤:

  • 把输入参数 a 赋值给 dw
  • 通过 w[0]w[1] 访问它的低16位和高16位
  • 使用 std::swap 交换这两个16位
  • 返回修改后的 dw

3. 这种写法的合法性:

  • 这里的 union 是你新建的局部变量,不是对传入变量强制类型转换。
  • 你通过 union 的成员访问它内部存储的数据,符合 C++ 标准对 union 访问的要求
  • 这种用法是 标准安全的,符合 strict aliasing 规则。

4. 关于 int16_t 是否合适

  • 你用的是带符号的 int16_t,交换后高位的符号位也会被交换,这通常没问题。
  • 但如果你想处理的是纯粹的二进制位,通常更推荐用 无符号类型 uint16_t,避免符号扩展或其他意外行为。

5. 总结

代码是否标准合法说明
你这段匿名 union 版本局部变量,直接访问union成员,合法且安全
之前 reinterpret_cast 指针版本违反 strict aliasing,UB

改进版建议(用无符号16位)

uint32_t swaphalves_union(uint32_t a) 
{ union { uint32_t dw; uint16_t w[2]; };dw = a;std::swap(w[0], w[1]);return dw;
}

你写的这段代码用到了 GCC/Clang 特有的 __attribute__((__may_alias__)),这是一个用来**告诉编译器“这个类型的指针可能别名其他类型”**的扩展属性。

代码分析

uint32_t swaphalves_mayalias(uint32_t a) 
{ using uint16_alias = uint16_t __attribute__((__may_alias__)); auto ptr = reinterpret_cast<uint16_alias*>(&a); std::swap(ptr[0], ptr[1]); return a; 
}

这个属性的作用

  • __may_alias__ 让编译器知道用这个类型的指针访问内存时,不要因为 strict aliasing 规则而假设它不会别名其他类型。
  • 相当于告诉编译器:“别因为别名规则就乱优化这块内存访问”

这样写的效果

  • 避免了因为严格别名规则导致的 未定义行为(UB)
  • 使得用 uint16_alias* 去访问 uint32_t 对象的内存更安全一些(至少在 GCC/Clang 下)。
  • 这个是 GCC 和 Clang 的扩展,非标准 C++,不能跨编译器使用

是否推荐?

优点缺点
能避免 strict aliasing 带来的 UB非标准扩展,不可移植
允许直接用 16 位指针访问 32 位对象只在 GCC/Clang 有效,MSVC 不支持
代码简洁,性能好依赖编译器扩展

标准做法的对比

方法标准合法性备注
__may_alias__(GCC/Clang)部分非标准非标准扩展,性能好
union 访问标准合法安全,代码稍冗长
位操作交换标准合法简单、跨平台
reinterpret_cast 指针非法UB,可能触发奇怪错误

总结

  • 如果你的项目只用 GCC/Clang,且在乎性能,可以用这个技巧。
  • 如果追求代码跨编译器兼容,最好用位操作或者 union 访问方式。
  • 不推荐在 MSVC 或其它编译器中使用,因为不支持这个属性。

你这里提到的命令:

g++ -std=c++11 -O3 -fno-strict-aliasing alias.cpp -o alias

和你给的代码:

uint32_t swaphalves(uint32_t a) 
{ auto ptr = reinterpret_cast<uint16_t*>(&a); std::swap(ptr[0], ptr[1]); return a; 
}

你的问题的背景和分析

1. -fno-strict-aliasing 的作用

  • 这个编译选项告诉 GCC 关闭严格别名规则的优化
  • 这样编译器就不会假设不同类型的指针不会别名,也就避免了一些因别名规则导致的未定义行为问题。
  • 你使用它的原因,通常是为了“允许”通过不同类型指针访问同一内存。

2. 代码本身的问题

你的代码是:

  • uint32_t 地址转换成 uint16_t*
  • 然后交换这两个 16 位部分
    根据 C++ 标准,这违反了 strict aliasing 规则,本应是未定义行为(UB)。
    但你使用了 -fno-strict-aliasing 选项后:
  • 编译器不再因为别名规则做优化假设
  • 程序在大多数情况下会正常运行

3. 结论和理解

  • 使用 -fno-strict-aliasing 是 GCC 提供的一种“妥协”方式,避免某些别名相关的编译优化导致程序出错。
  • 它会带来一定性能损失,因为编译器的别名优化被关闭。
  • 你给的代码在开启这个选项时是可行的,运行时结果符合预期。
  • 但是,这仍然不是标准推荐做法。

4. 推荐做法总结

方案标准兼容优缺点
使用 -fno-strict-aliasing兼容别名,性能下降,不推荐
union 访问成员标准合法,稍复杂但安全
位操作实现(`(a<<16)(a>>16)`)简洁、高效、跨平台,强烈推荐

5. 你这条命令的用法建议

如果你要快速跑通代码,不用修改逻辑,使用:

g++ -std=c++11 -O3 -fno-strict-aliasing alias.cpp -o alias

是可以的。

你的这段代码用 std::memcpy 实现了交换 uint32_t 的高低 16 位,整体思路是把内存内容拷贝到新变量中,调整顺序后返回,避免了严格别名规则的问题。

代码分析

uint32_t swaphalves_memcpy(uint32_t a) 
{ uint32_t swapped; auto swapped_char = reinterpret_cast<char*>(&swapped); auto a_char = reinterpret_cast<char const*>(&a); std::memcpy(swapped_char, a_char + sizeof(uint16_t), sizeof(uint16_t)); std::memcpy(swapped_char + sizeof(uint16_t), a_char, sizeof(uint16_t)); return swapped; 
}

作用

  • a_char 是指向 a 内存的 char* 指针,按字节访问
  • 先把 a 的高 16 位(偏移 2 字节)拷贝到 swapped 的低 16 位位置
  • 再把 a 的低 16 位拷贝到 swapped 的高 16 位位置
  • 返回交换后的结果

优点

  • 完全标准合规,不会触发严格别名规则的未定义行为
  • memcpy 访问内存是安全的,因为标准允许任何对象用 char* 类型访问内存
  • 代码可移植,跨平台

缺点

  • 比位运算稍慢(实际差异在绝大多数场景几乎可以忽略)
  • 代码相比位运算复杂一点,但安全性更高

总结

实现方式标准合法性能说明
位操作 `(a<<16)(a>>16)`最高简洁高效,推荐首选
union 访问成员很快方便且标准,需注意符号问题
memcpy 拷贝较快最安全的字节级内存访问方式
直接指针转换访问可能最好违反严格别名规则,不推荐

总结一下几点:

1. 低层代码和网络(反)序列化中的别名问题

  • 在操作底层内存、网络数据包,序列化/反序列化时,常常需要通过不同类型指针访问同一内存区域。
  • 这时候严格别名规则带来的限制和潜在的未定义行为非常容易出现。
  • 这些代码里经常会见到绕过别名规则的技巧和编译器特定的开关。

2. -fno-strict-aliasing 的使用

  • Linux 内核、libevent 等一些大型项目确实使用了 -fno-strict-aliasing 选项,来避免别名规则带来的问题。
  • 它是一个权宜之计,能够减少潜在的编译器优化带来的错误,但会牺牲部分性能。
  • 这说明实际项目中,标准严格别名规则的“硬”限制确实存在痛点。

3. 关于 placement new

  • GCC 关于使用 placement new(定位 new)相关的“合法别名”问题支持不够明确或者说存在争议。
  • placement new 重新构造对象,在别名和生命周期方面的细节,编译器和标准的支持及解释可能会影响别名规则的判断。
  • 因此,有时候代码会陷入模糊地带,难以用纯标准方式解决别名问题。

4. alias_cast 方案

  • alias_cast 是一种用来安全转换指针类型的工具(类似于 bit_castreinterpret_cast),设计时兼顾了别名规则。
  • 典型实现会利用 memcpy 或特殊的 union,保证转换操作既标准合法,又高效
  • 它是应对别名规则严格限制的优雅方案,尤其适合需要在不同类型间“重解释”数据的场景。

结论总结

主题说明
低层代码和网络数据操作经常碰到别名规则带来的挑战,需要特殊处理
-fno-strict-aliasing业界常用折中方案,但有性能和标准合规问题
placement new别名和对象生命周期问题复杂,编译器支持不够统一
alias_cast未来或已有的优雅解决方案,兼顾安全、标准和性能
http://www.xdnf.cn/news/782857.html

相关文章:

  • 业务材料——半导体行业MES系统核心功能工业协议AI赋能
  • 不确定性分析在LEAP能源-环境系统建模中的整合与应用
  • 论文中pdf图片文件太大怎么办
  • GPTBots在AI大语言模型应用中敏感数据匿名化探索和实践
  • 无人机自主降落论文解析
  • TypeScript 高级类型深度指南:从类型体操到实战设计
  • vue入门环境搭建及demo运行
  • 生成JavaDoc文档
  • 用 Vue 做一个轻量离线的“待办清单 + 情绪打卡”小工具
  • 项目课题——基于ESP32的智能插座
  • 华为数据之道 精读——【173页】读书笔记【附全文阅读】
  • VsCode 安装 Cline 插件并使用免费模型(例如 DeepSeek)
  • SQL进阶之旅 Day 13:CTE与递归查询技术
  • 3.2 HarmonyOS NEXT跨设备任务调度与协同实战:算力分配、音视频协同与智能家居联动
  • 【Harmony OS】数据存储
  • VScode自动添加指定内容
  • NLP学习路线图(二十一): 词向量可视化与分析
  • 大语言模型评测体系全解析(上篇):基础框架与综合评测平台
  • 虚荣虚无的对立统一
  • 电阻电容的选型
  • html基础01:前端基础知识学习
  • webstrom中git插件勾选提交部分文件时却出现提交全部问题怎么解决
  • SpringBoot3.2新特性:JdbcClient
  • Trae CN IDE自动生成注释功能测试与效率提升全解析
  • 在linux系统上搭建git服务器(ssh协议)
  • RTC实时时钟DS1338Z-33/PT7C433833WEX国产替代FRTC1338S
  • 【Kotlin】高阶函数Lambda内联函数
  • Elasticsearch | 如何将修改已有的索引字段类型并迁移数据
  • MongoDB账号密码笔记
  • mybatis打印完整的SQL,p6spy