【发布时间】:2014-01-15 01:04:56
【问题描述】:
我偶然发现了这段代码,用于在不使用临时变量或按位运算符的情况下交换两个整数。
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); 中具有未定义的行为,因为它不包含任何确定评估顺序的序列点。
我的问题是:这是交换两个整数的可接受解决方案吗?
【问题讨论】:
@EdHeal “为了几个字节”听起来像电影中的陈词滥调“为了几个生命”。如果你从这个角度来看,你听起来就像一个怪物。 |=^]
请记住,保存的“代码行”和“变量”不会加速您的程序的性能...相反,它们可能会损害它。我敢打赌,如果您查看链接的 ASM,它可能需要比简单的解决方案更多的指令和更多的周期来执行。
标签: c++ c swap undefined-behavior sequence-points
【解决方案1】:
没有。这是不可接受的。此代码调用Undefined behavior。这是因为没有定义对b 的操作。在表达式中
a=(a+b)-(b=a);
由于缺少sequence point,不确定是先修改b还是将其值用于表达式(a+b)。
看看什么标准的syas:
C11:6.5 表达式:
如果标量对象的副作用相对于同一标量对象的不同副作用或使用相同标量的值的值计算是无序的 对象,行为未定义。 如果有多个允许的排序 表达式的子表达式,如果这样的未排序的一面,则行为未定义 效果发生在任何排序中。84)1。
阅读C-faq- 3.8 和answer 以获取有关序列点和未定义行为的更详细说明。
1。重点是我的。
【讨论】:
@LightnessRacesinOrbit;应该证明什么来解释这一点? OP 不确定它是否可以接受,我澄清了这一点。
你说的是真的。目前这只是一个没有证据的断言。通常我们会引用此类事情的标准。
@mafso;不。这是未定义的行为。等待。我要引用标准。
@JackM: 编译器、日期和时间、室温、程序启动时恰好位于数据段 0xFC0143FC 中的位、你的宠物狗的名字……
这个答案的一个问题是,标准引用使用了sequencing这个新概念,而问题和答案讲的是sequence points。 排序取代序列点。而sequencing与sequence points有很大的不同。实际上,一些以前未定义的表达式在切换到新概念后变得完美定义,尤其是在 C++ 中使用赋值运算符的表达式。确实,该表达式在任何一个下都未定义,但混合起来仍然可能令人困惑。
【解决方案2】:如果你使用 gcc 和 -Wall 编译器已经警告你
a.c:3:26: 警告:“b”上的操作可能未定义 [-Wsequence-point]
从性能的角度来看,是否使用这样的构造也是值得商榷的。当你看
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++) 代码,它的功能基本相同,但考虑了move
#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
考虑到 gcc 的警告并没有看到任何技术收益,我会说,坚持使用标准技术。如果这成为瓶颈,您仍然可以调查,如何改进或避免这一小段代码。
【解决方案3】:您可以使用XOR swap algorithm 来防止任何溢出问题,并且仍然有一个单行。
但是,既然您有一个c++ 标签,我更喜欢简单的std::swap(a, b) 以使其更易于阅读。
【讨论】:
这没有回答问题。该问题专门询问显示的代码是否可以接受,而不是其他内容是否更好。
嗯,它确实回答了“为什么 a=(a+b)-(b=a) 是交换两个整数的错误选择?”问题,考虑标签...
@RobertJacobs 来自原始问题:“我的问题是 - 这是交换两个整数的可接受解决方案吗?”。
除非 Wikipedia 中介绍的算法在一般情况下不起作用——例如,您不能使用它来实现一个函数,例如,该函数通常被调用(并且可能两者都有引用相同的变量)。
【解决方案4】:2010 发回了一个问题,示例完全相同。
a = (a+b) - (b=a);
Steve Jessop 警告:
顺便说一句,该代码的行为是未定义的。 a 和 b 都被读取 并且在没有插入序列点的情况下编写。首先, 编译器完全有权在之前评估 b=a 评估 a+b。
这是2012 中发布的问题的解释。请注意,由于缺少括号,示例并不完全相同,但答案仍然相关。
在 C++ 中,算术表达式中的子表达式没有时间 订购。
a = x + y;
是先计算 x 还是先计算 y?编译器可以选择其中一个,也可以 选择完全不同的东西。评价顺序不 与运算符优先级相同:运算符优先级是严格的 已定义,并且评估顺序仅定义为粒度 你的程序有序列点。
事实上,在某些架构上,可以发出以下代码: 同时计算 x 和 y - 例如,VLIW 架构。
现在来自 N1570 的 C11 标准报价:
附件 J.1/1
在以下情况下是未指定的行为:
——顺序 评估子表达式的位置以及哪一侧的顺序 效果发生,除了函数调用(),&&, ||、? : 和逗号运算符 (6.5)。
——赋值运算符的操作数的求值顺序 (6.5.16)。
附件 J.2/1
在以下情况下是未定义的行为:
— 标量对象的副作用相对于任一 对同一标量对象或值计算的不同副作用 使用相同标量对象的值 (6.5)。
6.5/1
表达式是一系列运算符和操作数,用于指定 计算一个值,或指定一个对象或函数,或 产生副作用,或执行其组合。 运算符的操作数的值计算是有序的 在算子结果的值计算之前。
6.5/2
如果标量对象上的副作用相对于其中任何一个而言是未排序的 对同一标量对象或值的不同副作用 使用相同标量对象的值进行计算,行为是 不明确的。如果有多个允许的排序 表达式的子表达式,如果这样的表达式的行为是未定义的 未排序的副作用发生在任何排序中。84)
6.5/3
运算符和操作数的分组由语法指示。85) 除非稍后指定,副作用和价值计算 子表达式是无序的。86)
您不应依赖未定义的行为。
一些替代方案:在 C++ 中,您可以使用
std::swap(a, b);
异或交换:
a = a^b; b = a^b; a = a^b; 【解决方案5】:
除了其他用户调用的问题之外,这种类型的交换还有另一个问题:域。
如果a+b 超出限制怎么办?假设我们使用从0 到100 的数字。 80+ 60 会超出范围。
【解决方案6】:我的问题是 - 这是交换两个整数的可接受解决方案吗?
谁可以接受?如果你问我是否可以接受,那不会通过我参与的任何代码审查,相信我。
为什么 a=(a+b)-(b=a) 是交换两个整数的糟糕选择?
出于以下原因:
1) 正如您所注意到的,C 不能保证它确实做到了这一点。它可以做任何事情。
2) 假设它确实交换了两个整数,就像在 C# 中那样。 (C# 保证副作用从左到右发生。)代码仍然是不可接受的,因为它的含义完全不明显!代码不应该是一堆巧妙的技巧。为必须阅读和理解它的人编写代码。
3) 同样,假设它有效。该代码仍然是不可接受的,因为这完全是错误的:
我偶然发现了这段代码,用于在不使用临时变量或按位运算符的情况下交换两个整数。
这完全是错误的。这个技巧使用一个临时变量来存储a+b 的计算。该变量由编译器代表您生成,没有给出名称,但它就在那里。如果目标是消除临时工,这只会使情况变得更糟,而不是更好!你为什么要首先消除临时工?它们很便宜!
4) 这仅适用于整数。除了整数之外,还有很多东西需要交换。
简而言之,将时间花在编写明显正确的代码上,而不是试图想出实际上使事情变得更糟的巧妙技巧。
【解决方案7】:a=(a+b)-(b=a) 至少有两个问题。
您提到自己:缺少序列点意味着行为未定义。因此,任何事情都可能发生。例如,不能保证首先评估哪个:a+b 或 b=a。编译器可以选择先为赋值生成代码,或者做一些完全不同的事情。
另一个问题是有符号算术的溢出是未定义的行为。如果a+b 溢出,则无法保证结果;甚至可能抛出异常。
【讨论】:
缺少序列点意味着这是UB。没有理由只期望两种可能结果中的一种。例如,结果可能取决于某个暂存寄存器中先前的值。
@mafso:错误。和错误的报价。您提供的报价仅适用于 积分算术转换,一般不适用于有符号算术。 conversions 一直都是这样。同时,有符号整数算术期间的溢出会触发未定义的行为。 C11在这方面没有做任何改变。上面的答案是绝对正确的。 6.5/5 “如果在评估表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。”
@Joni:有符号整数运算期间的溢出会导致 C 和 C++ 中的未定义行为。两种语言都同意这一点。 mafso 的评论不正确。该评论中的引用取自语言规范的不相关部分。
你是对的......它是 UB,至少在 C 中不是实现定义的。抱歉。
感谢@AndreyT,我怀疑在看到未定义行为的一个例子是C11 3.4.3 中整数溢出的行为 之后会出现这种情况,但不能找不到正确的引用位置
【解决方案8】:除了关于未定义的行为和样式的其他答案之外,如果您编写的简单代码仅使用临时变量,编译器可能会跟踪这些值而不是在生成的代码中实际交换它们,而稍后只使用交换的值在某些情况下。它不能用你的代码做到这一点。在微优化方面,编译器通常比你好。
因此,您的代码可能更慢、更难理解,并且可能还有不可靠的未定义行为。
【讨论】:
这是我认为的答案。现代编译器非常聪明,一些架构在其管道中具有专门的路由来进行交换,因为交换是一种非常常见的操作。所以答案是:只需使用std::swap() 并让编译器决定什么更有效。
我应该补充一点,@Manu343726 好点。如果您使用 std::swap ,那么 if 使用该技巧会更快,编译器编写器希望有专门的 std::swap 来完成该技巧。
【解决方案9】:声明:
a=(a+b)-(b=a);
调用未定义的行为。引用段落中的第二个应被违反:
(C99, 6.5p2) “在前一个序列点和下一个序列点之间,一个对象的存储值最多只能通过表达式的评估修改一次。此外,应仅读取先验值以确定要存储的值。"
【解决方案10】:问题是根据C++标准
除非另有说明,否则对单个运算符的操作数进行求值 和单个表达式的子表达式是无序的。
所以这个表达式
a=(a+b)-(b=a);
有未定义的行为。
【讨论】:
@Manu343726 我没有提到序列点。这句话来自 C++ 14 草案。