在这个上下文中,1) If a side effect on a scalar object is un-sequenced relative to another side effect on the same scalar object, the behavior is undefined.
// 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++标准方面非常好。它还涉及到一些相关的未定义行为,例如这是未定义的行为,因为未定义行为是什么。
f(++i, ++i);
和f(i=1, i=-1);
。在第一个相关情况中,不清楚第一个参数是否应该是i+1
,第二个参数是否应该是i+2
或者反之;在第二个情况中,在函数调用后i
是否应该是1或-1也不清楚。这两种情况都是未定义行为,因为它们属于以下规则:
如果对标量对象的副作用与同一标量对象上的另一个副作用无序,则行为未定义。
因此,f(i=-1, i=-1)
也是未定义行为,因为它遵循相同的规则,尽管程序员的意图(在我看来)是明显且明确的。
保罗·德拉珀在他的结论中也明确表示:
这让我们想到了一个问题:“为什么会将它可能是已定义的行为吗?是的。它被定义了吗?没有。
f(i=-1, i=-1)
定义为未定义行为?”
为什么会这样
虽然C++标准中有一些疏漏(可能是粗心),但很多省略都有明确的理由和特定目的。虽然我知道这个目的通常是“让编译器编写工作更轻松”或“生成更快的代码”,但我主要想知道为什么要把f(i=-1, i=-1)
定义为UB。
harmic和supercat提供了主要答案,为UB提供了一个理由。Harmic指出,优化编译器可能会将表面上原子赋值操作分解成多个机器指令,并进一步交错这些指令以实现最佳速度。这可能会导致一些非常惊人的结果:在他的情况下,i
最终变成-2!因此,harmic演示了如果操作未排序,则对变量多次赋相同的值可能会产生不良影响。
supercat提供了一个相关的阐述,说明试图让f(i = -1,i = -1)
执行它看起来应该执行的操作的陷阱。他指出,在某些体系结构中,同时向同一内存地址进行多个写入是有严格限制的。如果我们处理的不是比f(i = -1,i = -1)
更微不足道的东西,编译器可能很难捕捉到这一点。
davidf还提供了一个与harmic非常相似的交错指令的示例。
尽管harmic、supercat和davidf的例子都有些牵强,但综合起来仍然提供了一个具体的理由,说明为什么f(i=-1, i=-1)
应该是未定义行为。我接受了harmic的答案,因为它最好地解决了所有why的含义,尽管Paul Draper的答案更好地解决了“为什么”的部分。
其他答案 JohnB指出,如果考虑到重载赋值运算符(而不仅仅是普通标量),那么我们也可能遇到问题。
std::nullptr_t
以及这些类型的cv限定版本(3.9.3)统称为标量类型。” - Rob Kennedyf(i-1, i = -1)
或类似的东西。 - Mr Listerf(i = -1, i = -2)
是未定义的,如果两个值相等,没有真正的理由例外。这将使标准和编译器更加复杂,而几乎没有任何好处。 - Phil1970