第5.13章: 严格别名规则

English

概述

严格别名规则是 C++ 中最微妙、最危险的部分之一。 它允许编译器假定:指向不同且不相关类型的指针 永远不会引用同一块内存。

在实践中,严格别名规则常常被程序员忽视、 误解,或者无意中违反。

违反该规则会导致未定义行为,即使代码“看起来正确”, 也可能悄无声息地产生错误结果。

在本章中,你将学到:

1. 什么是别名?

当两个指针指向同一个对象时,它们互为别名。


int x{42};

int *p = ::std::addressof(x);
int *q = ::std::addressof(x);   // p and q alias the same int

如果其中一个修改了对象,另一个就会看到变化。 当这些指针具有不同类型时,别名就变得危险了。

2. 严格别名规则说了什么

编译器可以假定:指向不相关类型的指针不会别名同一对象。

“不相关类型”指的是不属于以下情况的类型:

如果你通过指向不相关类型的指针访问某个对象,编译器就有权 假定这种访问永远不会发生 → 未定义行为。

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. 检查原始内存的安全方式

以下写法始终是安全的:

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”

你现在不必理解每一个细节。 暂时把注意力放在核心信息上: 通过指针进行类型双关是危险的,而严格别名规则正是原因。

要点