#define swap(a, b) (((a) ^ (b)) && ((a) ^= (b) ^= (a) ^= (b)))
或者
#define swap(a, b) (((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b)))
我看到了第二个宏(链接),但是不明白为什么它没有像第一个那样被编写?有我错过的特殊原因吗?
#define swap(a, b) (((a) ^ (b)) && ((a) ^= (b) ^= (a) ^= (b)))
或者
#define swap(a, b) (((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b)))
我看到了第二个宏(链接),但是不明白为什么它没有像第一个那样被编写?有我错过的特殊原因吗?
首先,这两个例子在C99和C11中都会引发未定义行为(undefined behavior)。
在C99中,可以理解为,由于缺少序列点(sequence points),它们将会引发未定义行为(undefined behavior)。
在上一个序列点和下一个序列点之间,对象的存储值最多只能被表达式的计算修改一次。此外,先前的值只能被访问以确定要存储的值。
解释:
第一个例子在两个序列点之间两次修改了a
的值,因此根据语句:在上一个序列点和下一个序列点之间,对象的存储值最多只能被表达式的计算修改一次,行为是未定义的。就这样(不需要考虑b
)。
C11文档说:
如果对标量对象的副作用(unsequenced)相对于同一标量对象的其他不同副作用或使用同一标量对象的值计算的值计算来说是无序的,则行为是未定义的(undefined behavior)。如果一个表达式的子表达式有多个允许的排序方式,那么如果在任何排序中出现这样的无序副作用,则其行为是未定义的84)。
在(a) ^= (b) ^= (a) ^= (b)
中,对a
的副作用是无序的,因此会引发未定义行为(undefined behavior)。需要注意的是,C11 6.5 p1说:
[...] 运算符的操作数的值计算在运算符结果的值计算之前。
这保证了在以下情况下:
(a) ^= (b) ^= (a) ^= (b)
| | | |
1 2 3 4
保证所有子表达式1、2、3和4在最左边的^=
运算符的结果计算之前计算完毕。但这并不保证表达式3的副作用在最左边的^=
运算符的结果值计算之前得到保证。
1. 强调是我的。
,
。 - Uchia Itachib ^= a ^= b
仍然是未定义的行为。 - Oliver Charlesworthb
的值计算将在a ^= b
结果的值计算之前进行排序,因此在分配给b
之前。所以我不明白它为什么是UB。但在C99中可能是UB。 - interjay(b) ^= (a) ^= (b)
的情况下无法应用。 - haccks第一个会在C99中调用未定义的行为,有两个原因,最明显的是,由于在同一序列点内不允许多次修改相同的变量,而该宏会多次修改a
和b
。而第二个则使用了逗号操作符:
#define swap(a, b) (((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b)))
^
该段介绍了C99中的序列点,但并不能消除所有未定义行为,因为在计算a
的值时需要读取b
的先前值,但只能用于确定要存储到b
的值。
C99草案标准第6.5节“表达式”第2段相关内容如下(以下加粗部分为重点):
在前一个序列点和下一个序列点之间,一个对象通过表达式的评估最多只能被修改一次72)。此外,先前的值只能被读取以确定要存储的值73)。
而对于逗号运算符,从第6.5.17节“逗号运算符”第2段中可以得知:
逗号运算符的左操作数被评估为无类型表达式; 其评估后有一个序列点。[...]
a
和b
的先前值被使用,这就是为什么添加逗号运算符不能消除所有未定义行为的原因。 - Shafik Yaghmourb ^= a ^= b
中没有足够的序列点。即使添加逗号运算符,仍然不会增加足够的序列点。 - Oliver Charleswortha = a^(b=b^(a=a^b))
对于等号后第一个出现的a,C编译器可以选择使用a的初始值或修改后的值。因此,这显然是不确定的,并导致未定义的行为。
第二个看起来对我来说没问题,没有歧义:
b = b ^(a=a^b)