第5.13章: 严格别名规则
概述
严格别名规则是 C++ 中最微妙、最危险的部分之一。 它允许编译器假定:指向不同且不相关类型的指针 永远不会引用同一块内存。
在实践中,严格别名规则常常被程序员忽视、 误解,或者无意中违反。
违反该规则会导致未定义行为,即使代码“看起来正确”, 也可能悄无声息地产生错误结果。
在本章中,你将学到:
- 别名是什么意思
- 严格别名规则说了什么
- 为什么该规则在真实代码中常被忽视
- 别名何时被允许
- 别名何时被禁止
- 为什么基于指针的类型双关很危险
- 如何安全地检查原始字节
- 如何使用
::std::bit_cast安全地重新解释位 - 如何把
[[__gnu__::__may_alias__]]作为最后的逃生口 - 到哪里进一步学习(Timur Doumler 的 CppCon 2019 演讲)
1. 什么是别名?
当两个指针指向同一个对象时,它们互为别名。
int x{42};
int *p = ::std::addressof(x);
int *q = ::std::addressof(x); // p and q alias the same int
如果其中一个修改了对象,另一个就会看到变化。 当这些指针具有不同类型时,别名就变得危险了。
2. 严格别名规则说了什么
编译器可以假定:指向不相关类型的指针不会别名同一对象。
“不相关类型”指的是不属于以下情况的类型:
- 同一类型(忽略
const/volatile) - 彼此的限定版本
- 同一 struct/class 层次中的成员
-
char、unsigned char或std::byte可用于 检查任何对象的原始字节 -
重要:
signed char、uint8_t、uint_least8_t和char8_t不能用于检查原始字节。 它们是彼此不同的类型,且不享有特殊的别名豁免。
如果你通过指向不相关类型的指针访问某个对象,编译器就有权 假定这种访问永远不会发生 → 未定义行为。
3. 为什么严格别名规则常被忽视
许多程序员来自那些类型双关无害的语言,或者他们
从旧书中学会了使用 reinterpret_cast 风格技巧的 C。
结果就是,真实世界的代码库经常包含:
- 不相关类型之间的指针转换
- 通过联合体进行类型双关
- 用错误的指针类型重新解释内存
这些写法在某些编译器或架构上可能“看起来能工作”,但它们 仍是未定义行为,在优化下可能失效。
严格别名违规常常看起来能工作,这一事实并不能让它们变得安全。
4. 允许的别名
同一类型(忽略 const)
int x{42};
int *p = ::std::addressof(x);
int const *q = ::std::addressof(x); // ok
char / unsigned char / std::byte 视图
int x{0x12345678};
unsigned char *bytes =
reinterpret_cast(::std::addressof(x));
读取这些字节始终是允许的。
5. 禁止的别名(未定义行为)
通过不相关类型的指针访问对象
float f{1.0f};
int *p = reinterpret_cast<int*>(::std::addressof(f));
// *p = 0; // ❌ undefined behavior
通过 reinterpret_cast 转换后的指针进行类型双关
int x{42};
float *pf = reinterpret_cast<float*>(::std::addressof(x));
// float y = *pf; // ❌ undefined behavior
6. 为什么存在严格别名规则
严格别名规则使编译器能进行激进的优化。
如果编译器能假定不相关类型的指针不互为别名,它就能:
- 让值在寄存器中停留更久
- 重排载入与存储
- 消除冗余的内存访问
违反该规则会打破这些假定,导致不可预测的行为。
7. 检查原始内存的安全方式
以下写法始终是安全的:
- 只通过其自身类型(或 cv 限定变体)的指针访问对象
- 使用
char、unsigned char或std::byte检查原始字节 - 避免基于指针的类型双关
8. 用 ::std::bit_cast 安全地进行类型双关
在第2.13章: 类型转换中,我们介绍了
::std::bit_cast,作为重新解释对象原始位的现代、安全方式。
它是危险的基于指针类型双关的正确替代品。
如果你需要把一个值的位重新解释为另一种类型,
请使用 ::std::bit_cast。不要使用指针转换。
#include <bit>
float f{1.0f};
// Safe: reinterpret the bits of float as std::uint32_t
std::uint32_t u = ::std::bit_cast<std::uint32_t>(f);
错误(未定义行为)
float f{1.0f};
std::uint32_t *p = reinterpret_cast<std::uint32_t*>(::std::addressof(f));
// std::uint32_t u = *p; // ❌ undefined behavior
正确
float f{1.0f};
std::uint32_t u = ::std::bit_cast<std::uint32_t>(f); // ✔ safe
9. 作为最后手段:[[__gnu__::__may_alias__]](GCC 扩展)
如果你确实必须通过指针建立别名,GCC 提供了一个非标准的逃生口:
[[__gnu__::__may_alias__]]。
这不是标准 C++。它不可移植,只应由完全理解 严格别名规则的专家使用。
// Detect whether [[__gnu__::__may_alias__]] is supported (GCC and Clang)
#if __has_cpp_attribute(__gnu__::__may_alias__)
# define MAY_ALIAS [[__gnu__::__may_alias__]]
#else
# define MAY_ALIAS
#endif
// Create an alias type for "int*" that is allowed to alias anything
using may_alias_int_ptr = int * MAY_ALIAS;
float f{1.0f};
// Allowed by GCC and Clang (non-portable)
may_alias_int_ptr p = reinterpret_cast(::std::addressof(f));
// GCC/Clang will not assume this violates strict aliasing
int bits = *p;
该属性只对那个类型禁用严格别名规则。
尽可能优先使用 ::std::bit_cast。
10. 推荐演讲:现代 C++ 中的类型双关
要深入了解严格别名与类型双关的现代解释,请观看 Timur Doumler 在 CppCon 2019 的这场演讲:
“Type punning in modern C++ – Timur Doumler – CppCon 2019”
你现在不必理解每一个细节。 暂时把注意力放在核心信息上: 通过指针进行类型双关是危险的,而严格别名规则正是原因。
要点
- 严格别名规则很微妙,在真实代码中常被忽视。
- 通过不相关类型的指针访问对象是未定义行为。
char、unsigned char与std::byte可以安全检查原始字节;signed char、uint8_t、uint_least8_t与char8_t则不可以。- 基于指针的类型双关几乎总是未定义行为。
::std::bit_cast是重新解释位的现代、安全方式。[[__gnu__::__may_alias__]]是 GCC/Clang 专属的逃生口;它不可移植,不应当依赖它。- 严格别名规则使编译器能进行激进优化。
- 观看 Timur Doumler 的 CppCon 2019 演讲可获得更深理解。