我正在阅读有关评估违规的顺序,它们举了一个令人困惑的例子。
1)如果相对于同一标量对象上的另一个副作用,未对标量对象上的副作用进行排序,则该行为未定义。
// snip f(i = -1, i = -1); // undefined behavior
在这种情况下,i
是标量对象,这显然意味着
算术类型(3.9.1),枚举类型,指针类型,指向成员类型的指针(3.9.2),std :: nullptr_t和这些类型的cv限定版本(3.9.3)统称为标量类型。
在这种情况下,我看不出该陈述的模棱两可。在我看来,无论是第一个还是第二个参数都首先被求值,结果i
都为-1
,并且两个参数也都是-1
。
有人可以澄清一下吗?
更新
我非常感谢所有讨论。到目前为止,我非常喜欢@harmic的答案,因为尽管乍一看看上去很直截了当,但它暴露了定义此语句的陷阱和复杂性。@ acheong87指出了使用引用时出现的一些问题,但我认为这与该问题的无序副作用方面是正交的。
摘要
既然这个问题引起了很多关注,我将总结要点/答案。首先,请允许我指出一下“为什么”可能具有紧密相关但又微妙的不同含义,即“出于何种原因 ”,“出于何种原因 ”和“出于何种目的 ”。我将按照回答“为什么”的含义中的哪一个分组答案。
是什么原因
这里的主要答案来自Paul Draper,Martin J做出了类似但不太广泛的答案。保罗·德雷珀(Paul Draper)的答案归结为
这是未定义的行为,因为未定义行为是什么。
就解释C ++标准所说的内容而言,答案总体来说是非常好的。它还解决了UB的一些相关情况,例如f(++i, ++i);
和f(i=1, i=-1);
。在第一种相关情况下,不清楚第一个参数是否应为i+1
第二个参数,i+2
反之亦然;在第二个中,不清楚i
函数调用后应为1还是-1。这两种情况都是UB,因为它们属于以下规则:
如果相对于相同标量对象上的另一个副作用,未对标量对象上的副作用进行排序,则该行为未定义。
因此,f(i=-1, i=-1)
UB也是如此,因为它属于同一规则,尽管程序员(IMHO)的意图是明显且明确的。
保罗·德雷珀(Paul Draper)在其结论中也明确指出:
可以定义行为吗?是。定义好了吗?没有。
这带给我们一个问题:“由于什么原因/目的f(i=-1, i=-1)
留下未定义的行为?”
由于什么原因/目的
尽管C ++标准中存在一些疏漏(可能是粗心大意),但是许多遗漏是有道理的,并且可以满足特定的目的。尽管我知道目的通常是“使编译器-编写器的工作更轻松”或“更快的代码”,但我主要还是想知道是否有充分理由离开 f(i=-1, i=-1)
UB。
harmic和supercat提供了提供UB 原因的主要答案。Harmic指出,一个优化的编译器可能会将表面上的原子分配操作分解为多个机器指令,并且可能会进一步交错这些指令以实现最佳速度。这可能会导致一些非常令人惊讶的结果:i
在他的情况下最终为-2!因此,harmic演示了如果不对操作进行排序,那么多次将相同的值分配给变量会产生不良影响。
超级猫提供了尝试f(i=-1, i=-1)
去做它应该做的陷阱的相关陷阱。他指出,在某些体系结构上,对多次同时写入同一内存地址有严格的限制。如果我们处理的琐碎事情比编译器小,编译器可能会很难抓住它f(i=-1, i=-1)
。
davidf还提供了一个与谐波指令非常相似的交织指令示例。
尽管每个谐波,超级猫和davidf的示例在某种程度上都是人为设计的,但将它们合在一起,仍然可以提供一个明确的理由,说明为什么f(i=-1, i=-1)
应采用不确定的行为。
我接受harmic的答案是因为,即使Paul Draper的答案更好地解决了“原因”部分,它也尽了最大努力解决了所有原因。
其他答案
JohnB指出,如果我们考虑重载的赋值运算符(而不只是普通的标量),那么我们也会遇到麻烦。
f(i-1, i = -1)
或类似的意思。
std::nullptr_t
和这些类型的cv限定版本(3.9.3)统称为标量类型。 ”