作为参考,我试图理解本文中的示例:
:
总结如下:
int foo(int *a, long *b)
{
int t = *a;
*b = 0; // cannot change *a
return *a - t; // can be folded to zero
}
int bar(int *a, long *b)
{
int t = *a;
for (int i = 0; i != sizeof *b; ++i)
((unsigned char*)b)[i] = 0;
return *a - t; // must not be folded
}
文章声称foo
的回报是可以折叠的:
由于
a
和b
被声明为指向不兼容类型的指针,并且由于 C 和 C++ 要求对象的存储值只能由兼容类型的左值访问,因此存储到不能影响缓存在变量中*b
的值(通常是寄存器)。因此,减法表达式中的操作数必须相等,并且结果必须为零。*a
t
但我不明白为什么在 中会有所不同bar
。哪些代码/事件序列会导致折叠无效?
12
最佳答案
2
unsigned char
类型别名规则对于通过类型(以及char
和)的 glvalue 访问任何类型的对象有一个特定的例外std::byte
。对于这些类型,并且只有这些类型,在别名规则下不存在编译器可以利用进行优化的未定义行为。C 具有与别名规则类似的例外,但在对象模型方面有不同规定。
如果a
和b
表示相同的地址,则*b = 0
或int t = *a;
必定具有未定义的行为,因为在该地址处只能存在int
或一个long
对象,并且只能访问该对象。另一种访问要么尝试访问超出生存期的对象,要么尝试通过不同类型的泛左值访问一种类型的对象,从而违反类型别名规则。其中一个必须导致未定义的行为,因此编译器可以假设这种情况(即a
和表示相同的地址)不会发生。(指向和对象的b
内存范围也不能重叠,因为对象和对象不能同时存在于重叠存储中。)int
long
int
long
在第二个示例中,*b = 0
被替换为((unsigned char*)b)[i] = 0
。 指针b
永远不会作为long
类型访问,而只能作为 访问unsigned char
。 由于上述例外,如果a
和b
都指向同一类型的对象,int
即它们都表示的地址处 (唯一) 存活的对象,则根据别名规则,这不是 UB。 因此,编译器不能假设a
和b
表示不同的地址。
话虽如此,C++ 标准缺乏关于通过 glvalue 进行访问unsigned char
或指针上的指针算法unsigned char*
应该如何表现的任何实际定义,因此,从严谨的角度阅读该标准无论如何它仍然是 UB。
但是,通常的假设是,它的行为应该好像unsigned char*
转换的指针结果是指向原始对象的对象表示的指针,被视为数组unsigned char
。这在很多边缘情况下仍然留下了很多不清楚的地方,但解释了常见用法通常如何按预期工作。
基于这种理解,将直接修改上述有效场景中指向的对象((unsigned char*)b)[i] = 0
的对象表示。更改对象的对象表示意味着更改其值,因此编译器不能假设后面的读取将产生与写入之前相同的值,而必须重新加载它。int
a
int
*a
|
- 在 中
foo
,赋值*b = 0
不会产生影响,*a
因为在严格的别名规则下类型不兼容。这使得编译器可以安全地折叠*a - t
为零。 - 然而,在 bar 中,将 b 强制转换为
unsigned char*
并逐字节写入意味着编译器必须考虑到*a
在循环中被修改的可能性。因此,*a - t
不能假设 的值是零,并且编译器必须避免折叠此表达式。
关键区别在于是unsigned char*
一种绕过严格别名规则的特殊类型,允许通过间接修改int
指向的对象。a
b
|
*b = 0;
,a
永远不会通过 引用b
,也b
永远不会通过 引用a
,因此根本不存在别名问题。如果意图是您不应该通过 引用a
,b
反之亦然,那只是严格别名规则本身的陈述。long
和int
不是兼容类型。 的值*a
不会神奇地缓存在 中t
,t
它只是函数的一个局部变量,存储在 持有的地址中的值被赋值a
给该函数。这段话不清楚,不要过多解读。–
–
char*
、unsigned char*
和std::byte*
。en.cppreference.com /…(这是 C++;C 规则不同)–
–
–
|