使用异或运算交换数值

3
这两个宏有什么区别?
#define swap(a, b)    (((a) ^ (b)) && ((a) ^= (b) ^= (a) ^= (b)))

或者

#define swap(a, b)    (((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b)))

我看到了第二个宏(链接),但是不明白为什么它没有像第一个那样被编写?有我错过的特殊原因吗?


@quamrana 感谢您的格式化。 - yunusaydin
and also @user3075488 - yunusaydin
3个回答

4

首先,这两个例子在C99和C11中都会引发未定义行为(undefined behavior)

在C99中,可以理解为,由于缺少序列点(sequence points),它们将会引发未定义行为(undefined behavior)

C-faq:

在上一个序列点和下一个序列点之间,对象的存储值最多只能被表达式的计算修改一次。此外,先前的值只能被访问以确定要存储的值。

解释:
第一个例子在两个序列点之间两次修改了a的值,因此根据语句:在上一个序列点和下一个序列点之间,对象的存储值最多只能被表达式的计算修改一次,行为是未定义的。就这样(不需要考虑b)。

C11文档说:

6.5 表达式 (p2):

如果对标量对象的副作用(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. 强调是我的。


2
第二个中有一个 , - Uchia Itachi
2
@UchiaItachi:不要认为这会有所不同。b ^= a ^= b仍然是未定义的行为。 - Oliver Charlesworth
@OliCharlesworth 这真的是未定义行为吗?在C11中,我认为右侧b的值计算将在a ^= b结果的值计算之前进行排序,因此在分配给b之前。所以我不明白它为什么是UB。但在C99中可能是UB。 - interjay
1
@interjay:在C99中,这绝对是未定义行为。如果C11增加了进一步的顺序约束,那很好,但在全世界大多数人使用C11之前,使用C99作为基线可能更安全...(或者至少在答案中明确说明这一区别)。 - Oliver Charlesworth
@OliCharlesworth; 好的。在C99或C11中,第二个不会引发未定义行为。然而,“先前的值只能被访问以确定要存储的值”这一规则在(b) ^= (a) ^= (b)的情况下无法应用。 - haccks

2

第一个会在C99中调用未定义的行为,有两个原因,最明显的是,由于在同一序列点内不允许多次修改相同的变量,而该宏会多次修改ab。而第二个则使用了逗号操作符

#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段中可以得知:

逗号运算符的左操作数被评估为无类型表达式; 其评估后有一个序列点。[...]


3
难道不只有一个原因吗?也就是缺乏序列点吗? - Oliver Charlesworth
1
@OliCharlesworth,第二个原因是除了确定要存储的值之外,ab的先前值被使用,这就是为什么添加逗号运算符不能消除所有未定义行为的原因。 - Shafik Yaghmour
2
但这正是导致 UB 的原因,因为在 b ^= a ^= b 中没有足够的序列点。即使添加逗号运算符,仍然不会增加足够的序列点。 - Oliver Charlesworth
@OliCharlesworth 这是一个公正的观点,但正如第二段代码所观察到的那样,它们有微妙的不同原因,显然有人认为这可以消除所有未定义的行为。 - Shafik Yaghmour
@ShafikYaghmour; 我认为我们错了。在C99中,第二个也不会引起未定义的行为。语句“先前的值只能被读取以确定要存储的值”与“(b) ^= (a) ^= (b)”无关。 - haccks

1
为了更好地理解为什么第一个未定义,这里有另一种表述方式:
这是因为在C中,您无法控制子表达式的执行顺序:
a = a^(b=b^(a=a^b))

对于等号后第一个出现的a,C编译器可以选择使用a的初始值或修改后的值。因此,这显然是不确定的,并导致未定义的行为。

第二个看起来对我来说没问题,没有歧义:

b = b ^(a=a^b)

在表达式的第一部分中出现a和b似乎对我来说不是问题,因为&&强制先评估第一部分。但是,我更喜欢让专家解剖标准,我不是专家...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接