c++ - 为什么 f(i = -1, i = -1) 未定义行为?

标签 c++ language-lawyer undefined-behavior

我正在阅读 order of evaluation violations ,他们举了一个让我困惑的例子。

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是一个标量对象,这显然意味着

Arithmetic types (3.9.1), enumeration types, pointer types, pointer to member types (3.9.2), std::nullptr_t, and cv-qualified versions of these types (3.9.3) are collectively called scalar types.



在这种情况下,我不明白该声明是如何模棱两可的。在我看来,无论是先评估第一个参数还是第二个参数,i最终为 -1 , 并且两个参数也是 -1 .

有人可以澄清吗?

更新

我非常感谢所有的讨论。到目前为止,我喜欢@harmic’s answer很多,因为它暴露了定义这个语句的陷阱和复杂性,尽管它乍一看是多么直截了当。 @acheong87指出了使用引用时出现的一些问题,但我认为这与此问题的未排序副作用方面是正交的。

概括

由于这个问题引起了大量关注,我将总结要点/答案。首先,请允许我稍微跑题一下,指出“为什么”可以具有密切相关但又略有不同的含义,即“出于什么原因”、“出于什么原因”和“出于什么目的”。我将根据他们解决的“为什么”的含义对答案进行分组。

什么原因

这里的主要答案来自Paul Draper , 与 Martin J提供类似但不那么广泛的答案。保罗德雷珀的回答归结为

It is undefined behavior because it is not defined what the behavior is.



就解释 C++ 标准所说的内容而言,答案总体上非常好。它还解决了一些UB的相关案例,例如f(++i, ++i);f(i=1, i=-1); .在第一个相关案例中,不清楚第一个参数是否应该是 i+1第二个 i+2反之亦然;在第二个中,不清楚是否 i函数调用后应为 1 或 -1。这两种情况都是 UB,因为它们属于以下规则:

If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.



因此,f(i=-1, i=-1)也是 UB,因为它属于同一规则,尽管程序员的意图(恕我直言)是显而易见的和明确的。

保罗·德雷珀 (Paul Draper) 在他的结论中也明确指出

Could it have been defined behavior? Yes. Was it defined? No.



这让我们想到了“出于什么原因/目的 f(i=-1, i=-1) 被保留为未定义行为?”的问题。

出于什么原因/目的

尽管 C++ 标准中存在一些疏忽(可能是粗心大意),但许多疏漏是有充分理由的,并且服务于特定目的。尽管我知道目的通常是“使编译器编写者的工作更轻松”或“更快的代码”,我主要是想知道是否有充分的理由离开 f(i=-1, i=-1) 作为 UB。

harmicsupercat提供为 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)应该是未定义的行为。

我接受了哈米奇的回答,因为它在解决所有原因的含义方面做得最好,尽管保罗·德雷珀的回答更好地解决了“原因”部分。

其他答案

JohnB指出如果我们考虑重载赋值运算符(而不仅仅是简单的标量),那么我们也会遇到麻烦。

最佳答案

由于操作是无序的,因此没有什么可以说执行分配的指令不能交错。这样做可能是最佳选择,具体取决于 CPU 架构。引用的页面说明了这一点:

If A is not sequenced before B and B is not sequenced before A, then two possibilities exist:

  • evaluations of A and B are unsequenced: they may be performed in any order and may overlap (within a single thread of execution, the compiler may interleave the CPU instructions that comprise A and B)

  • evaluations of A and B are indeterminately-sequenced: they may be performed in any order but may not overlap: either A will be complete before B, or B will be complete before A. The order may be the opposite the next time the same expression is evaluated.



这本身似乎不会导致问题 - 假设正在执行的操作将值 -1 存储到内存位置。但是也没有什么可以说编译器无法将其优化为具有相同效果的单独指令集,但如果该操作与同一内存位置上的另一个操作交错,则可能会失败。

例如,想象一下,与加载值 -1 相比,将内存归零然后递减它更有效。 那么这个:
f(i=-1, i=-1)

可能会变成:
clear i
clear i
decr i
decr i

现在我是-2。

这可能是一个虚假的例子,但它是可能的。

关于c++ - 为什么 f(i = -1, i = -1) 未定义行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21670459/

相关文章:

c++ - 使用列表进行动态结构分配

c++ - 将 xvalues 转换为 lvalues 以传递给函数是否定义明确?

c++ - 将数组作为结构体访问与未定义的行为

c++ - 我可以使用 C++ 中的内置类型安全地新建 [],然后转换指针,然后删除 [] 吗?

c++ - Variadic 类模板和继承 - 默认编译器生成的构造函数

c++ - 公共(public)结构继承私有(private)结构 C++

python - 如何使用 native Python/C API 从 C++ 创建 python 自定义类型?

c - 关于在 printf 函数中使用时指向整数的未初始化指针的行为问题

c++ - 在标准下调用 std::function<void(Args...)> 是否非法?

c++ - 正在通过 const ref undefined 行为捕获新构造的对象