考虑以下三个表达式:
++x;
x += 1;
x = x + 1;
据我所知,它们在语义上是相同的,忽略了C ++中的运算符重载。然而,今天我读到了一个断言,它们是不同的,特别是在何时 x
宣布 volatile
。
为了测试这个断言,我编写了以下内容并为PowerPC,AMD64,ARMv6和68k编译了它:
#include <stdint.h>
static volatile uint64_t x = 0;
void a(void)
{
++x;
}
void b(void)
{
x += 1;
}
void c(void)
{
x = x + 1;
}
在所有这四个平台上,这三个函数产生相同的汇编输出,无论是在-O1还是-O3。在AMD64上,这只是两条指令:
incq _x(%rip)
retq
因此, 是 那个断言背后有什么真相吗?如果是这样,有什么区别,我该如何揭露它?
NB:我完全清楚这一点 volatile
不保证原子性。这不是我在这里要求的 - 除非原子性本身在三者之间有所不同。
从草案C ++标准部分 5.3.2
[expr.pre.incr] 说:
如果x不是bool类型,则表达式++ x等效于x + = 1
和 5.17
[expr.ass] 说:
E1 op = E2形式的表达式的行为等同于
E1 = E1操作E2除了E1仅被评估一次。
所以 ++x
和 x += 1
是等价的。
现在一个案例在哪里 x += 1
不同于 x = x + 1
就是它 E1
仅评估一次。在这种特殊情况下,它并不重要,但我们可以提出一个案例:
#include <stdint.h>
volatile uint64_t x = 0;
volatile uint64_t y[2] = {0} ;
void c(void)
{
y[x] = y[x] + 1;
}
在这种情况下 x
将被评估两次,而不是这种情况:
void b(void)
{
y[x] += 1;
}
和a godbolt会议显示 对于 b()
:
b(): # @b()
movq x(%rip), %rax
incq y(,%rax,8)
retq
并为 c()
:
c(): # @c()
movq x(%rip), %rax
movq y(,%rax,8), %rax
incq %rax
movq x(%rip), %rcx
movq %rax, y(,%rcx,8)
retq
据我所知,这也适用于C11。从C11部分 6.5.3.1
前缀增量和减量运算符:
表达式++ E等价于(E + = 1)。
从部分 6.5.16.2
复合赋值:
形式E1 op = E2的复合赋值相当于简单
赋值表达式E1 = E1 op(E2),除了左值为E1
仅评估一次
在抽象语义中,所有这三个表达式都完全相同。他们 访问 x
检索其值,计算新值,然后将更新后的值存储回来 x
。有一个访问和商店。 (表达式也会产生一个被丢弃的值)。
虽然 x = x + 1
提到 x
两次,左侧 x
没有评估。也就是说,并非完全:它 值 没有计算。仅根据确定指定值的位置来评估它。
所以这里可能会对位置进行双重评估:左侧确定位置 x
右侧也是如此。但确定位置不涉及访问位置本身。
对于某些类型的表达式,确定位置确实涉及访问值。例如:
a[i] = a[i] + 1;
这是完全不同的
i = i + 1
因为 i
这里只是一个辅助变量,必须知道其值才能确定存储位置 a[i]
(和 i
本身甚至不会增加)。如果 i
是 volatile
,然后是两个抽象访问它 a[i] = a[i] + 1
必须对应两个实际访问。