为什么a =(a + b) – (b = a)是交换两个整数的不好的select?

我偶然发现了这个代码,用于交换两个整数而不使用临时variables或使用按位运算符。

int main(){ int a=2,b=3; printf("a=%d,b=%d",a,b); a=(a+b)-(b=a); printf("\na=%d,b=%d",a,b); return 0; } 

但是我认为这个代码在交换语句a = (a+b) - (b=a);有未定义的行为a = (a+b) - (b=a); 因为它不包含任何序列点来确定评估的顺序。

我的问题是: 这是一个可接受的解决scheme交换两个整数?

不,这是不可接受的。 此代码调用未定义的行为 。 这是因为对b的操作没有定义。 在expression式中

 a=(a+b)-(b=a); 

由于缺less序列点,不能确定b首先被修改,或者它的值被用在expression式( a+b )中。
看看什么标准的协议:

C11:6.5expression式:

如果对标量对象的副作用不是相对于同一个标量对象的不同副作用或使用相同标量对象的值进行值计算而言是不确定的,则行为是未定义的。 如果一个expression式的子expression式有多个可允许的sorting顺序,那么如果这种不顺序的副作用发生在任何顺序中,行为是不确定的

阅读C-faq-3.8和这个答案可以更详细地解释序列点和未定义的行为。


重点是我的。

我的问题是 – 这是一个可接受的解决scheme交换两个整数?

可以接受谁? 如果你问我是否可以接受,那么不会超过我所在的任何代码审查,相信我。

为什么a =(a + b) – (b = a)交换两个整数是不好的select?

出于以下原因:

1)如你所说,在C中没有保证它实际上是这样做的。 它可以做任何事情。

2)假设为了争辩,它确实交换了两个整数,就像在C#中一样。 (C#保证副作用从左到右发生。)代码仍然是不可接受的,因为它的意义完全不明显! 代码不应该是一堆聪明的技巧。 为需要阅读和理解的人员编写代码。

3)再次假设它有效。 代码仍然是不可接受的,因为这只是错误的:

我偶然发现了这个代码,用于交换两个整数而不使用临时variables或使用按位运算符。

那简直是错误的。 这个技巧使用一个临时variables来存储a+b的计算。 这个variables是由编译器以你的名义生成的,而不是一个名字,但是它在那里。 如果目标是消除临时,这使情况变得更糟,而不是更好! 为什么你想要消除临时工呢? 他们很便宜!

4)这只适用于整数。 很多东西需要交换,而不是整数。

简而言之,把时间花在专注于编写明显正确的代码上,而不是想出一些让事情变得更糟的巧妙技巧。

a=(a+b)-(b=a)至less有两个问题。

你提到你自己:缺乏顺序点意味着行为是不确定的。 因此,任何事情都可能发生。 例如,不能保证首先评估哪一个: a+bb=a 。 编译器可以select首先为作业生成代码,或者做一些完全不同的事情。

另一个问题是,有符号算术的溢出是不确定的行为。 如果a+b溢出,则不能保证结果。 甚至可能抛出exception。

如果你使用gcc和-Wall ,编译器已经警告你

ac:3:26:警告:'b'上的操作可能是未定义的[-W序列点]

是否使用这样的结构也是从性能angular度来说是有争议的。 当你看

 void swap1(int *a, int *b) { *a = (*a + *b) - (*b = *a); } void swap2(int *a, int *b) { int t = *a; *a = *b; *b = t; } 

并检查汇编代码

 swap1: .LFB0: .cfi_startproc movl (%rdi), %edx movl (%rsi), %eax movl %edx, (%rsi) movl %eax, (%rdi) ret .cfi_endproc swap2: .LFB1: .cfi_startproc movl (%rdi), %eax movl (%rsi), %edx movl %edx, (%rdi) movl %eax, (%rsi) ret .cfi_endproc 

您可以看到混淆代码没有好处。


看看C ++(g ++)代码,它基本上是一样的,但需要考虑

 #include <algorithm> void swap3(int *a, int *b) { std::swap(*a, *b); } 

给出相同的组件输出

 _Z5swap3PiS_: .LFB417: .cfi_startproc movl (%rdi), %eax movl (%rsi), %edx movl %edx, (%rdi) movl %eax, (%rsi) ret .cfi_endproc 

考虑到海湾合作委员会的警告,并没有看到技术上的收获,我坚持使用标准技术。 如果这成为瓶颈,你仍然可以调查,如何改善或避免这一小块代码。

除了其他的答案之外,关于未定义的行为和风格,如果你编写简单的代码只使用一个临时variables,那么编译器可能会跟踪这些值,而不是实际上在生成的代码中交换它们,而只是稍后使用交换后的值案例。 它不能用你的代码。 在微型优化中,编译器通常比你更好。

所以很可能你的代码更慢,更难理解,也可能是不可靠的未定义行为。

该声明:

 a=(a+b)-(b=a); 

调用未定义的行为。 第二段在引用段落中被违反:

(C99,6.5p2)“在上一个和下一个序列点之间,一个对象的存储值最多只能通过一个expression式的评估修改一次, 而且,只有在读取之前的值才能确定要存储的值。

一个问题是在2010年发布的,同样的例子。

 a = (a+b) - (b=a); 

Steve Jessop警告说:

那个代码的行为是不确定的,顺便说一下。 a和b都是没有中间顺序点的情况下读写的。 对于初学者来说,在评估a + b之前,编译器完全有权评估b = a。

以下是2012年发布的问题的解释。 请注意,样本不完全相同 ,因为缺less括号,但答案仍然相关。

在C ++中,算术expression式中的子expression式没有时间顺序。

a = x + y;

是先评估x还是y? 编译器可以select,也可以select完全不同的东西。 评估顺序与运算符优先级不一样:运算符优先级严格定义, 评估顺序仅限于程序具有顺序点的粒度。

事实上,在某些体系结构中,可以发出同时评估x和y的代码 – 例如VLIW体系结构。

现在来自N1570的C11标准报价:

附件J.1 / 1

在下列情况下是未指定的行为:

– 除了为函数调用()&&||指定外,子expression式的评估顺序和副作用发生的顺序。 , ? : ? :和逗号运算符(6.5)。

– 赋值运算符的操作数的评估顺序(6.5.16)。

附件J.2 / 1

这是未定义的行为时:

– 对标量对象的副作用是相对于相同标量对象的不同副作用或使用相同标量对象(6.5)的值计算的不相关的。

6.5 / 1

expression式是一系列运算符和操作数,它们指定值的计算,或者指定对象或函数,或者产生副作用,或者执行其组合。 运算符操作数的值计算在运算符结果的值计算之前被sorting。

6.5 / 2

如果对标量对象的副作用不是相对于同一个标量对象的不同副作用或使用相同标量对象的值进行值计算而言是不确定的,则行为是未定义的。 如果一个expression式的子expression式有多个可允许的sorting,那么如果这种无序的副作用发生在任何顺序中,则行为是不确定的。

6.5 / 3

除了后面所述,子expression式的副作用和值计算是没有序列化的。

你不应该依赖未定义的行为。

一些替代scheme:在C ++中,你可以使用

  std::swap(a, b); 

XOR交换:

  a = a^b; b = a^b; a = a^b; 

问题是根据C ++标准

除了注意到的地方之外,对个别操作符和个别expression式的操作数的评估是不确定的。

所以这个expression

 a=(a+b)-(b=a); 

有未定义的行为。

您可以使用XOR交换algorithm来防止任何溢出问题,并且仍然有一个单线程。

但是,既然你有一个c++标记,我宁愿只是一个简单的std::swap(a, b) ,使其更容易阅读。

除了其他用户调用的问题之外,这种交换还有另一个问题:

如果a+b超出限制呢? 假设我们使用从010080+ 60会超出范围。