此代码违反了严格的别名规则,这使得通过不同类型的指针访问对象是非法的,尽管允许通过 *char ** 进行访问。允许编译器假设不同类型的指针不指向同一个内存并进行相应的优化。这也意味着代码调用了未定义的行为并且真的可以做任何事情。
该主题的最佳参考之一是Understanding Strict Aliasing,我们可以看到第一个示例与OP 的代码类似:
uint32_t swap_words( uint32_t arg )
{
uint16_t* const sp = (uint16_t*)&arg;
uint16_t hi = sp[0];
uint16_t lo = sp[1];
sp[1] = hi;
sp[0] = lo;
return (arg);
}
文章解释说这段代码违反了严格的别名规则,因为sp
它是的别名,arg
但它们有不同的类型,并说虽然它会编译,但返回arg
后很可能不会改变swap_words
。尽管通过简单的测试,我无法使用上面的代码或 OPs 代码重现该结果,但这并不意味着任何事情,因为这是未定义的行为,因此不可预测。
本文继续讨论了许多不同的情况,并提出了几种可行的解决方案,包括通过联合进行类型双关,这在C99 1中定义良好,在C++中可能未定义,但实际上大多数主要编译器都支持,例如这里是gcc 关于 type-punning 的参考。上一篇 C 和 C++ 中联合的目的的线程进入了血腥的细节。尽管关于这个主题有很多线程,但这似乎做得最好。
该解决方案的代码如下:
typedef union
{
uint32_t u32;
uint16_t u16[2];
} U32;
uint32_t swap_words( uint32_t arg )
{
U32 in;
uint16_t lo;
uint16_t hi;
in.u32 = arg;
hi = in.u16[0];
lo = in.u16[1];
in.u16[0] = lo;
in.u16[1] = hi;
return (in.u32);
}
作为参考,C99 标准草案中关于严格别名的相关部分是6.5
Expressions第7段,其中说:
对象的存储值只能由具有以下类型之一的左值表达式访问:76)
— 与对象的有效类型兼容的类型,
— 与对象的有效类型兼容的类型的限定版本,
— 与对象的有效类型相对应的有符号或无符号类型,
— 对应于对象有效类型的限定版本的有符号或无符号类型,
— 在其成员中包含上述类型之一的聚合或联合类型(递归地,包括子聚合或包含联合的成员),或
— 一种字符类型。
脚注76说:
此列表的目的是指定对象可能或可能不别名的情况。
C++ 标准草案的相关部分是3.10
Lvalues and rvalues第10段
文章Type-punning and strict-aliasing对该主题进行了较为温和但不太完整的介绍,而C99 revisited则对C99和 aliasing进行了深入分析,阅读量并不大。访问非活动工会成员的这个答案 - 未定义?通过C++中的 union 讨论了类型双关语的混乱细节,并且阅读起来也不轻松。
脚注:
- 引用Pascal Cuoq 的评论:[...]C99 最初措辞笨拙,似乎使通过联合的类型双关语未定义。实际上,尽管工会的类型双关在 C89 中是合法的,在 C11 中是合法的,并且在 C99 中一直是合法的,尽管直到 2004 年委员会才修复了不正确的措辞,并随后发布了 TC3。open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm