【问题标题】:Why is *ptr = (*ptr)++ Undefined Behavior为什么 *ptr = (*ptr)++ 未定义行为
【发布时间】:2021-12-29 15:19:37
【问题描述】:

我正在尝试学习如何在以下情况(如下所示)中解释未定义行为的原因(如果有)。

int i = 0, *ptr = &i;
i = ++i; //is this UB? If yes then why according to C++11
*ptr = (*ptr)++; //i think this is UB but i am unable to explain exactly why is this so
*ptr = ++(*ptr); //i think this is not UB but can't explain why 

我查看了许多 SO 帖子,描述了与上述情况类似的不同指针情况的 UB,但我仍然无法解释 为什么准确(例如 使用哪个点我们可以证明它们会导致 UB 的标准) 它们会导致 UB

我正在寻找根据 C++11(或 C++14)而不是 C++17 和 Pre-C++11 的解释。

【问题讨论】:

  • 你看过哪些帖子?您在参考的相应标准中发现了什么?
  • @UlrichEckhardt 比如this。当然还有其他的。但我故意没有在我的问题中提及每个单独的帖子,因为我想要一个单独的答案,并且想让我的问题简短而切题。
  • 很棒的发现!它甚至为 C++11 提供了具体而详细的答案。我想知道您缺少哪些部分的解释...
  • thisthis
  • @NathanOliver 是的,我已经阅读了您链接的那些帖子。我仍然想要一个单独的答案。无论如何,谢谢。

标签: c++ c++11 pointers undefined-behavior expression-evaluation


【解决方案1】:

未定义的行为源于此:

C++11 [intro.execution]/15 除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的...如果对标量对象产生副作用相对于同一标量对象的另一个副作用或使用同一标量对象的值的值计算是无序的,则行为未定义。

C++17 [intro.execution]/17 除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是无序的……如果对内存位置有副作用(4.4) 相对于同一内存位置上的另一个副作用或使用同一内存位置中任何对象的值的值计算是无序的,并且它们不是潜在的并发 (4.7),行为未定义。

这段文字很相似。主要区别在于“除非特别注明”部分;在 C++17 中,为比在 C++11 中更多的运算符指定了操作数的求值顺序。因此:

C++17 [expr.ass]/1 在所有情况下,赋值顺序在值之后 计算右操作数和左操作数,以及在赋值表达式的值计算之前。 右操作数排在左操作数之前。

C++11 缺少粗体部分。这部分使 i = i++ 在 C++17 中定义良好,但在 C++11 中未定义。这是因为对于后缀增量,副作用不是表达式的值计算的一部分:

C++11和C++17 [expr.post.incr]/1++表达式的值计算在操作数对象修改之前排序。

所以“赋值在左右操作数的值计算之后排序”本身是不够的:赋值在i++的值计算之后排序,副作用也是在相同的值之后排序计算,但没有说明它们是如何相对于彼此排序的。因此,它们是未排序的,并且它们都在修改同一个对象(此处为i)。这表现出未定义的行为。

C++17中“右操作数在左操作数之前排序”的添加意味着i++的副作用在i的值计算之前排序,并且两者都在赋值之前排序。


另一方面,对于预增量,副作用必然是表达式评估的一部分:

C++11 和 C++17 [expr.pre.incr]/1 ... 结果是更新后的操作数;它是一个左值...

所以++i 的值计算涉及首先增加i,然后应用左值到右值的转换以获得更新后的值。在 C++11 和 C++17 中,这个值计算在赋值之前排序,因此i 的两个副作用是相对于彼此排序的;没有未定义的行为。


如果将i 替换为(*ptr),则此分析不会发生任何变化。这只是引用同一对象或内存位置的另一种方式。

【讨论】:

  • 哇,好分析。我确保我得到了正确的一切。同时 +1 来自我。
【解决方案2】:

C++ 标准基于 C 标准,其作者不需要任何特定的“理由”来说明实现可以以对其客户最有用的任何方式处理构造[这就是他们想要的短语“未定义的行为”的意思]。对于小型原始类型,许多平台可以廉价地保证涉及对同一对象的读取和冲突写入的竞争条件将始终产生旧数据或新数据,并且涉及冲突写入的竞争条件将导致每个后续读取看到其中一个书面价值观。该标准并没有试图确定实现应该或不应该支持保证的所有情况,而是允许实现在闲暇时“以环境特征的记录方式”处理代码。因为并非所有实现都在所有可能的场景中提供这种保证是不切实际的,而且因为这种保证可行的场景范围在不同的平台上会有所不同,所以标准的作者允许实现权衡提供的利弊在其特定目标平台上提供各种行为保证,而不是尝试编写适用于所有可能实现的精确规则。

还要注意,如果要执行以下操作:

*p = (*q)++;
return q[0] + q[i]; // where 'i' is some object of type `int`.

pq 相等且i 为零时,编译器可能会非常合理地生成代码,其中赋值会撤消增量的影响,但会返回q 的旧值的总和,加上 1,加上 q 的实际存储值(这将是旧值,而不是增加的值)。尽管这将是指定竞争条件语义的逻辑结果,但试图精确指定它会非常尴尬,以至于标准只是允许实现按照他们认为合适的方式严格或松散地指定行为。

【讨论】:

    猜你喜欢
    • 2010-12-22
    • 2021-12-28
    • 1970-01-01
    • 1970-01-01
    • 2019-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多