我正在阅读评估违规顺序,他们给出了一个让我困惑的例子。
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指出了使用引用时出现的一些问题,但我认为这与这个问题的未排序副作用方面是正交的。
由于这个问题得到了大量的关注,我将总结要点/答案。首先,请允许我稍微离题一点,指出“why”可能有密切相关但微妙不同的含义,即“for what 导致”,“for what 原因”和“for what 目的”。我将根据他们所说的“为什么”的意思来对答案进行分组。
这里的主要答案来自保罗•德雷伯,而马丁J提供了类似但不广泛的答案。保罗·德雷珀的答案可以归结为
它是未定义的行为,因为它没有定义行为是什么。
就解释c++标准的含义而言,这个答案总体上是非常好的。它还处理了一些UB的相关情况,如f(++i, ++i);
和f(i=1, i=-1);
。在第一个相关的情况下,不清楚第一个参数是否应该是i+1
,第二个参数是否应该是i+2
,反之亦然;在第二种情况下,不清楚i
在函数调用后应该是1还是-1。这两种情况都是UB,因为它们符合以下规则:
如果一个标量对象上的副作用相对于同一标量对象上的另一个副作用没有排序,则该行为是未定义的。
因此,f(i=-1, i=-1)
也是UB,因为它属于相同的规则,尽管程序员的意图是(恕我直言)明显和明确的。
Paul Draper在他的结论中也明确指出
它可以被定义为行为吗?是的。它被定义了吗?不。
这给我们带来了一个问题:“出于什么原因/目的,f(i=-1, i=-1)
被保留为未定义的行为?”
尽管c++标准中存在一些疏忽(可能是粗心大意),但许多遗漏都是合理的,并为特定的目的服务。虽然我知道这样做的目的通常是“让编译器编写者的工作更容易”,或者“更快的代码”,但我主要想知道你是否有充分的理由离开 f(i=-1, i=-1)
乌兰巴托。
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的回答,因为它在解释为什么的所有含义方面做得最好,尽管Paul Draper的回答更好地回答了“原因是什么”这部分。
JohnB指出,如果我们考虑重载赋值操作符(而不仅仅是普通的标量),那么我们也会遇到麻烦。