提问人:Benoit 提问时间:9/19/2008 最后编辑:NathanOliverBenoit 更新时间:5/16/2022 访问量:291278
什么是严格的混叠规则?
What is the strict aliasing rule?
答:
严格别名不允许使用不同的指针类型来访问相同的数据。
本文应帮助您全面详细了解该问题。
评论
int
int
通过指针强制转换(而不是使用联合)的类型双关语是打破严格别名的一个主要示例。
评论
fpsync()
我找到的最好的解释是 Mike Acton 的《理解严格混叠》。它有点专注于 PS3 开发,但这基本上只是 GCC。
来自文章:
“严格别名是由C(或C++)编译器做出的假设,即取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名)。
所以基本上,如果你指向某个包含内存的内存,然后你指向该内存并将其用作一个,你就打破了规则。如果你的代码不遵守这一点,那么编译器的优化器很可能会破坏你的代码。int*
int
float*
float
该规则的例外是 ,它允许指向任何类型。char*
评论
char *
遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区(如指向 s 或 s 的指针)上。当您通过指针转换将结构覆盖到此类缓冲区上,或将缓冲区覆盖到此类结构上时,很容易违反严格的别名规则。uint32_t
uint16_t
因此,在这种设置中,如果我想向某些东西发送消息,我必须有两个不兼容的指针指向同一个内存块。然后,我可能会天真地编写这样的代码:
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
严格的别名规则使此设置成为非法:取消引用指针,该指针为不兼容类型或 C 2011 6.5 第 7 1 段允许的其他类型之一的对象设置别名是未定义的行为。不幸的是,你仍然可以以这种方式编码,可能会得到一些警告,让它编译良好,只是在你运行代码时出现奇怪的意外行为。
(GCC 在发出别名警告的能力上似乎有些不一致,有时给我们一个友好的警告,有时则不然。
要了解为什么此行为未定义,我们必须考虑严格的别名规则会给编译器带来什么。基本上,使用此规则,它不必考虑插入指令来刷新每次循环运行的内容。相反,在优化时,通过一些令人讨厌的关于混叠的未强制假设,它可以省略这些指令,在循环运行之前加载并加载到 CPU 寄存器中一次,并加快循环的主体速度。在引入严格别名之前,编译器必须处于一种偏执状态,即任何先前的内存存储都可以更改其内容。因此,为了获得额外的性能优势,并假设大多数人不键入双关指针,引入了严格的别名规则。buff
buff[0]
buff[1]
buff
请记住,如果您认为该示例是人为的,那么如果您将缓冲区传递给另一个为您执行发送的函数,甚至可能会发生这种情况。
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
并重写了我们之前的循环,以利用这个方便的功能
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
编译器可能能够也可能不能或足够聪明地尝试内联 SendMessage,并且它可能会也可能不会决定再次加载或不加载 buff。如果是另一个单独编译的 API 的一部分,它可能有加载 buff 内容的指令。再说一次,也许你在 C++ 中,这是编译器认为它可以内联的一些模板化标头实现。或者,它可能只是您为了方便而在 .c 文件中编写的内容。无论如何,未定义的行为可能仍然随之而来。即使我们知道引擎盖下发生的一些事情,它仍然违反了规则,因此无法保证明确定义的行为。因此,仅仅通过包装一个采用我们的单词分隔缓冲区的函数并不一定有帮助。SendMessage
那么我该如何解决这个问题呢?
使用联合。大多数编译器都支持这一点,而不会抱怨严格的别名。这在 C99 中是允许的,在 C11 中是明确允许的。
union { Msg msg; unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)]; };
您可以在编译器中禁用严格别名(gcc 中的 f[no-]strict-aliasing))
您可以使用别名代替系统的单词。规则允许 (包括 和 ) 例外。始终假定别名为其他类型。但是,这不会以另一种方式工作:没有假设您的结构别名为字符缓冲区设置别名。
char*
char*
signed char
unsigned char
char*
初学者要当心
当两种类型相互叠加时,这只是一个潜在的雷区。您还应该了解字节序、单词对齐方式,以及如何通过正确打包结构来处理对齐问题。
脚注
1 C 2011 6.5 7 允许左值访问的类型有:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 一种类型,该类型是对应于对象的有效类型,是有符号或无符号类型,
- 一种类型,该类型是与对象的有效类型的限定版本相对应的有符号或无符号类型,
- 在其成员中包含上述类型之一的聚合或联合类型(以递归方式包括子聚合或包含联合的成员),或者
- 字符类型。
评论
unsigned char*
char*
unsigned char
char
byte
unsigned char *
unsigned int
uint32_t
unsigned int
uint32_t
uint32_t
unsigned int
严格别名不仅指针,它还会影响引用,我为 boost developer wiki 写了一篇关于它的论文,它非常受欢迎,以至于我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人们如此困惑以及如何应对它。严格混叠白皮书。特别是,它解释了为什么联合是 C++ 的危险行为,以及为什么使用 memcpy 是 C 和 C++ 之间唯一可移植的修复程序。希望这对您有所帮助。
评论
这是严格的别名规则,可在 C++03 标准的第 3.10 节中找到(其他答案提供了很好的解释,但没有提供规则本身):
如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象动态类型的 CV 限定版本,
- 一种类型,该类型是与对象的动态类型相对应的有符号或无符号类型,
- 一种类型,该类型是对应于对象的动态类型的 CV 限定版本的有符号或无符号类型,
- 在其成员中包含上述类型之一的聚合或联合类型(以递归方式包括子聚合或包含联合的成员),
- 一个类型,该类型是对象的动态类型的(可能是 CV 限定的)基类类型,
- a 或 type。
char
unsigned char
C++11 和 C++14 措辞(强调更改):
如果程序尝试通过以下类型之一以外的 glvalue 访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象动态类型的 CV 限定版本,
- 类似于对象的动态类型(如 4.4 中所定义)的类型,
- 一种类型,该类型是与对象的动态类型相对应的有符号或无符号类型,
- 一种类型,该类型是对应于对象的动态类型的 CV 限定版本的有符号或无符号类型,
- 在其元素或非静态数据成员中包含上述类型之一的聚合或联合类型(以递归方式包括子聚合或包含联合的元素或非静态数据成员),
- 一个类型,该类型是对象的动态类型的(可能是 CV 限定的)基类类型,
- a 或 type。
char
unsigned char
有两个小变化:glvalue 而不是 lvalue,以及对聚合/联合情况的澄清。
第三个变化提供了更强的保证(放宽了强锯齿规则):现在对别名安全的类似类型的新概念。
还有C措辞(C99;ISO/IEC 9899:1999 6.5/7;ISO/IEC 9899:2011 §6.5 ¶7 中使用了完全相同的措辞):
对象的存储值只能由左值访问 具有以下类型之一的表达式:73) 或 88):
- 与对象的有效类型兼容的类型,
- 与有效类型兼容的类型的限定版本 对象,
- 一种类型,该类型是与 对象的有效类型,
- 一种类型,该类型是与 对象有效类型的限定版本,
- 包含上述类型之一的聚合或联合类型 其成员中的类型(包括递归的 子聚合或包含的联合),或
- 字符类型。
73) 或 88) 此列表的目的是指定对象可能别名或可能不会别名的情况。
评论
S1
S2
int x;
int
void blah(S1 *p1, S2, *p2
p1->x
p2->x
int
wow(&u->s1,&u->s2)
u
作为 Doug T. 已经写过的内容的附录,这里 是一个简单的测试用例,可能会用 gcc 触发它:
检查.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
使用 .
通常(对于我尝试过的大多数 gcc 版本)这会输出“严格混叠问题”,因为编译器假设“h”不能与“check”函数中的“k”相同。因此,编译器会优化 away 并始终调用 printf。gcc -O2 -o check check.c
if (*h == 5)
对于那些感兴趣的人,这里是由 gcc 4.6.3 生成的 x64 汇编程序代码,在 ubuntu 12.04.2 for x64 上运行:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
因此,if 条件从汇编程序代码中完全消失了。
评论
long long*
int64_t
long long*
int64_t*
根据 C89 的基本原理,该标准的作者不希望要求编译器提供如下代码:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
应该要求在赋值和 return 语句之间重新加载 的值,以便允许可能指向 的可能性,并且赋值可能会因此改变 的值。编译器应该有权假定在上述情况下不会有别名的概念是没有争议的。x
p
x
*p
x
不幸的是,C89 的作者编写规则的方式,如果从字面上理解,甚至会使以下函数调用 Undefined Behavior:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
因为它使用 类型的左值来访问 类型的对象,并且不属于可用于访问 .因为将结构和联合的所有非字符类型成员的使用都视为未定义的行为是荒谬的,所以几乎每个人都认识到,至少在某些情况下,一种类型的左值可用于访问另一种类型的对象。不幸的是,C标准委员会未能定义这些情况是什么。int
struct S
int
struct S
大部分问题是由缺陷报告 #028 导致的,该报告询问了程序的行为,例如:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
缺陷报告 #28 指出,程序调用了未定义的行为,因为写入类型为“double”的联合成员并读取类型为“int”的联合成员的操作调用了实现定义的行为。这种推理是荒谬的,但构成了有效类型规则的基础,这些规则不必要地使语言复杂化,而对解决原始问题却无能为力。
解决原始问题的最佳方法可能是处理 关于该规则目的的脚注,就好像它是规范性的一样,并作出 该规则不可执行,除非实际涉及使用别名的冲突访问。给出类似的东西:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
内部没有冲突,因为通过访问的存储的所有访问都是使用 类型的左值完成的,并且没有冲突,因为明显派生自 ,并且到下次使用时,将对该存储进行的所有访问都将已经发生。inc_int
*p
int
test
p
struct S
s
p
如果代码稍有更改...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
在这里,标记行上的 和 的访问之间存在混叠冲突,因为在执行时存在另一个引用,该引用将用于访问同一存储。p
s.x
如果缺陷报告 028 说原始示例调用了 UB,因为这两个指针的创建和使用之间存在重叠,那么这将使事情变得更加清晰,而无需添加“有效类型”或其他此类复杂性。
评论
在阅读了许多答案之后,我觉得有必要补充一些东西:
严格混叠(我稍后会描述)很重要,因为:
内存访问可能很昂贵(性能方面),这就是为什么数据在写回物理内存之前在 CPU 寄存器中操作的原因。
如果两个不同 CPU 寄存器中的数据将被写入相同的内存空间,那么当我们用 C 语言编码时,我们无法预测哪些数据会“存活”。
在汇编中,我们手动编写 CPU 寄存器的加载和卸载代码,我们将知道哪些数据保持不变。但是C(谢天谢地)把这个细节抽象出来了。
由于两个指针可以指向内存中的同一位置,因此这可能会导致处理可能的冲突的复杂代码。
这种额外的代码很慢,并且会损害性能,因为它会执行额外的内存读/写操作,这些操作既慢又(可能)不必要。
严格别名规则允许我们避免冗余的机器代码,在这种情况下,假设两个指针不指向同一个内存块应该是安全的(另请参阅关键字)。restrict
严格别名表示,可以安全地假设指向不同类型的指针指向内存中的不同位置。
如果编译器注意到两个指针指向不同的类型(例如,an 和 a ),它将假定内存地址不同,并且不会防止内存地址冲突,从而导致机器代码速度更快。int *
float *
例如:
假设以下函数:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
为了处理以下情况(两个指针都指向同一个内存),我们需要对将数据从内存加载到 CPU 寄存器的方式进行排序和测试,因此代码可能最终如下所示:a == b
load 和 from memory。
a
b
搭。
a
b
保存并重新加载。
b
a
(从 CPU 寄存器保存到内存,并从内存加载到 CPU 寄存器)。
搭。
b
a
保存(从 CPU 寄存器)到内存。
a
步骤 3 非常慢,因为它需要访问物理内存。但是,需要防止指向同一内存地址的实例。a
b
严格的别名将允许我们通过告诉编译器这些内存地址明显不同来防止这种情况(在这种情况下,这将允许进一步的优化,如果指针共享内存地址,则无法执行)。
这可以通过两种方式告诉编译器,即使用不同的类型来指向。即:
void merge_two_numbers(int *a, long *b) {...}
使用关键字。即:
restrict
void merge_two_ints(int * restrict a, int * restrict b) {...}
现在,通过满足严格别名规则,可以避免步骤 3,并且代码的运行速度将大大加快。
事实上,通过添加关键字,整个功能可以优化为:restrict
load 和 from memory。
a
b
搭。
a
b
将结果保存到 和 。
a
b
这种优化以前是不可能完成的,因为可能存在碰撞(其中 和 将增加三倍而不是两倍)。a
b
评论
b
a
restrict
register
restrict
restrict
注意
本文摘自我的“什么是严格混叠规则,我们为什么关心?
什么是严格锯齿?
在 C 和 C++ 中,别名与允许我们访问存储值的表达式类型有关。在 C 和 C++ 中,该标准指定了允许哪些表达式类型为哪些类型添加别名。编译器和优化器可以假设我们严格遵循别名规则,因此称为严格别名规则。如果我们尝试使用不允许的类型访问值,则将其归类为未定义行为 (UB)。一旦我们有未定义的行为,所有的赌注都关闭了,我们程序的结果就不再可靠了。
不幸的是,在严格的别名违规的情况下,我们通常会得到我们期望的结果,从而有可能在未来版本的编译器中采用新的优化来破坏我们认为有效的代码。这是不可取的,了解严格的别名规则以及如何避免违反这些规则是一个值得的目标。
为了更多地了解我们为什么关心,我们将讨论在违反严格的别名规则时出现的问题,类型双关语,因为类型双关语中使用的常用技术经常违反严格的别名规则以及如何正确键入双关语。
初步示例
让我们看一些例子,然后我们可以准确地讨论标准所说的内容,检查一些进一步的例子,然后看看如何避免严格的锯齿并捕获我们遗漏的违规行为。这是一个不应该令人惊讶的例子(现场示例):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
我们有一个 int* 指向 int 占用的内存,这是一个有效的别名。优化器必须假定通过 ip 进行的赋值可以更新 x 占用的值。
下一个示例显示了导致未定义行为的别名(实时示例):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
在函数 foo 中,我们取一个 int* 和一个 float*,在这个例子中,我们调用 foo 并将这两个参数设置为指向同一个内存位置,在这个例子中,它包含一个 int。请注意,reinterpret_cast告诉编译器将表达式视为具有其模板参数指定的类型。在本例中,我们告诉它对待表达式 &x,就好像它具有 float* 类型一样。我们可能天真地期望第二个 cout 的结果是 0,但在使用 -O2 启用优化后,gcc 和 clang 都会产生以下结果:
0
1
这可能出乎意料,但完全有效,因为我们调用了未定义的行为。float 不能有效地别名 int 对象。因此,优化器可以假定在取消引用 i 时存储的常量 1 将是返回值,因为通过 f 存储无法有效地影响 int 对象。在编译器资源管理器中插入代码表明这正是正在发生的事情(实时示例):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
使用基于类型的别名分析 (TBAA) 的优化器假定将返回 1,并直接将常量值移动到携带返回值的寄存器 eax 中。TBAA 使用有关允许别名使用哪些类型的语言规则来优化加载和存储。在这种情况下,TBAA 知道浮点数不能别名 int,并优化了 i 的负载。
现在,进入规则手册
标准究竟说我们被允许和不允许做什么?标准语言并不简单,因此对于每个项目,我将尝试提供代码示例来演示其含义。
C11标准是怎么说的?
C11 标准在第 6.5 节表达式第 7 段中规定如下:
对象的存储值只能由具有以下类型之一的左值表达式访问:88) — 与对象的有效类型兼容的类型,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
— 与对象的有效类型兼容的类型的限定版本,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
— 一种类型,该类型是与对象的有效类型相对应的有符号或无符号类型,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc/clang 有一个扩展,并且允许将 unsigned int* 分配给 int*,即使它们不是兼容的类型。
— 一种类型,该类型是有符号或无符号类型,对应于对象有效类型的限定版本,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified version of the effective type of the object
— 在其成员中包含上述类型之一的聚合或联合类型(以递归方式包括子聚合或包含联合的成员),或者
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it
// can alias with *ip
foo f;
foobar( &f, &f.x );
— 字符类型。
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
C++17 标准草案是怎么说的
C++17标准草案在[basic.lval]第11段中说:
如果程序尝试通过以下类型之一以外的 glvalue 访问对象的存储值,则行为未定义:63
(11.1) — 对象的动态类型,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) — 对象动态类型的 CV 限定版本,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) — 类似于对象的动态类型的类型(如 7.5 中所定义),
(11.4) — 一种类型,该类型是与对象的动态类型相对应的有符号或无符号类型,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) — 一种类型,该类型是有符号或无符号类型,对应于对象动态类型的 CV 限定版本,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) — 一种聚合或联合类型,在其元素或非静态数据成员中包含上述类型之一(以递归方式包括子聚合或包含联合的元素或非静态数据成员),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) — 一种类型,该类型是对象的动态类型的(可能是 CV 限定的)基类类型,
struct foo { int x; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) — char、unsigned char 或 std::byte 类型。
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
值得注意的是,上面的列表中不包含带符号的字符,这与表示字符类型的 C 有显着区别。
什么是类型双关语
我们已经走到了这一步,我们可能想知道,我们为什么要别名?答案通常是键入双关语,通常使用的方法违反了严格的别名规则。
有时我们想绕过类型系统,将对象解释为不同的类型。这称为类型双关语,用于将一段内存重新解释为另一种类型。类型关语对于希望访问对象的基础表示形式以进行查看、传输或操作的任务非常有用。我们发现使用的典型类型双关语领域是编译器、序列化、网络代码等......
传统上,这是通过获取对象的地址,将其转换为我们想要重新解释的类型的指针,然后访问该值,或者换句话说,通过别名来实现的。例如:
int x = 1;
// In C
float *fp = (float*)&x; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x); // Not a valid aliasing
printf( "%f\n", *fp );
正如我们之前所看到的,这不是一个有效的别名,因此我们调用了未定义的行为。但传统上,编译器并没有利用严格的别名规则,这种类型的代码通常只是工作,不幸的是,开发人员已经习惯了以这种方式做事。类型双关语的常见替代方法是通过联合,这在 C 中有效,但在 C++ 中未定义行为(参见实时示例):
union u1
{
int n;
float f;
};
union u1 u;
u.f = 1.0f;
printf( "%d\n", u.n ); // UB in C++ n is not the active member
这在 C++ 中是无效的,有些人认为联合的目的仅仅是为了实现变体类型,并且认为使用联合进行类型双关是一种滥用。
我们如何正确输入双关语?
C 和 C++ 中类型双关的标准方法是 memcpy。这似乎有点沉重,但优化器应该认识到使用 memcpy 进行类型双关语并对其进行优化,并生成一个寄存器来寄存器移动。例如,如果我们知道 int64_t 的大小与 double 相同:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
我们可以使用 memcpy:
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
在足够的优化水平上,任何像样的现代编译器都会生成与前面提到的 reinterpret_cast 方法或 union 方法相同的代码,用于类型双关语。检查生成的代码,我们看到它只使用 register mov(实时编译器资源管理器示例)。
C++20 和 bit_cast
在 C++20 中,我们可能会获得 bit_cast(在提案的链接中提供实现),它提供了一种简单而安全的方式来键入双关语,并且可以在 constexpr 上下文中使用。
下面是一个示例,说明如何使用 bit_cast 将 pun a unsigned int 键入为 float,(实时查看):
std::cout << bit_cast<float>(0x447a0000) << "\n"; //assuming sizeof(float) == sizeof(unsigned int)
如果 To 和 From 类型的大小不同,则需要我们使用中间结构15。我们将使用一个包含 sizeof( unsigned int ) 字符数组的结构体(假设 4 字节无符号 int)作为 From 类型,将 unsigned int 作为 To 类型。
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {}; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result;
}
不幸的是,我们需要这种中间类型,但这是bit_cast当前的约束。
捕获严格的锯齿冲突
我们没有很多好的工具来捕获 C++ 中的严格锯齿,我们拥有的工具将捕获一些严格锯齿违规的情况以及一些负载和存储未对齐的情况。
gcc 使用标志 -fstrict-aliasing 和 -Wstrict-aliasing 可以捕获一些情况,尽管并非没有误报/误报。例如,以下情况将在 gcc 中生成警告(实时查看):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
虽然它不会捕捉到这个额外的情况(实时查看):
int *p;
p = &a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
尽管 clang 允许这些标志,但它显然实际上并没有实现警告。
我们可以使用的另一个工具是 ASan,它可以捕获未对齐的负载和存储。尽管这些不是直接的严格别名冲突,但它们是严格别名冲突的常见结果。例如,以下情况在使用 clang 使用 -fsanitize=address 构建时将生成运行时错误
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
我要推荐的最后一个工具是特定于 C++ 的工具,严格来说不是一个工具,而是一种编码实践,不允许 C 风格的强制转换。gcc 和 clang 都将使用 -would-style-cast 生成 C 样式强制转换的诊断。这将强制任何未定义的类型双关语使用 reinterpret_cast,通常reinterpret_cast应该成为更仔细的代码审查的标志。在代码库中搜索reinterpret_cast以执行审核也更容易。
对于 C,我们已经涵盖了所有工具,我们还有 tis-interpreter,这是一个静态分析器,可以详尽地分析程序中 C 语言的大部分子集。给定前面示例的 C 版本,其中使用 -fstrict-aliasing 会遗漏一个情况(实时查看)
int a = 1;
short j;
float f = 1.0;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p = &a;
printf("%i\n", j = *((short*)p));
tis-interpeter 能够捕获所有这三个,以下示例调用 tis-kernel 作为 tis-interpreter(为简洁起见,对输出进行了编辑):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
最后是目前正在开发的TySan。此清理程序在影子内存段中添加类型检查信息,并检查访问以查看它们是否违反别名规则。该工具可能应该能够捕获所有别名冲突,但可能会产生较大的运行时开销。
评论
reinterpret_cast
cout
cip
从技术上讲,在 C++ 中,严格的别名规则可能永远不适用。
注意间接(* 运算符)的定义:
一元 * 运算符执行间接操作:它所针对的表达式 应用应为指向对象类型的指针,或指向 函数类型,结果是引用对象的左值或 表达式指向的函数。
也来自 glvalue 的定义
glvalue 是一个表达式,其计算结果决定了 一个对象,(...剪)
因此,在任何定义良好的程序跟踪中,glvalue 都是指一个对象。因此,所谓的严格别名规则永远都不适用。这可能不是设计师想要的。
评论
int foo;
*(char*)&foo
char
foo
foo
char
char
int
int i;
in addition to one of type
? I see no way to apply a consistent definition of "object" which would allow for operations on both
i
volatile
评论