【问题标题】:Does the statement `int val = (++i > ++j) ? ++i : ++j;` invoke undefined behavior?语句 `int val = (++i > ++j) 吗? ++i : ++j;` 调用未定义的行为?
【发布时间】:2019-08-05 19:39:38
【问题描述】:

给定以下程序:

#include <stdio.h>
int main(void)
{
    int i = 1, j = 2;
    int val = (++i > ++j) ? ++i : ++j;
    printf("%d\n", val); // prints 4
    return 0;
}

val 的初始化似乎隐藏了一些未定义的行为,但我看不到对象被多次修改或在没有序列点的情况下修改和使用的任何点。有人可以 在这点上纠正或证实我?

【问题讨论】:

  • 有序列点吗?请参阅this answer,其中指出有一个 “在条件 ?: 运算符的第一个操作数的评估与第二个和第三个操作数中的任何一个被评估之间 (6.5.15)。”跨度>
  • 你得到了 4。你期望什么?
  • 我预计是 4。我认为这段代码不会调用 UB,但在另一个问题上有人告诉我它确实如此。只是想消除关于该特定陈述是否会导致 UB 的任何混淆,并可能得到比我在问题中提供的更好的解释。
  • 三元表达式保证序列点。我找不到任何对(++i &gt; ++j) 的引用。 &gt; 是序列点吗?
  • @Jean-FrançoisFabre 不,但不需要。两个不同变量的两次变化之间不需要序列点。 ++i &gt; ++i 不过是 UB。

标签: c ternary-operator sequence-points


【解决方案1】:

这段代码的行为是明确定义的。

保证条件中的第一个表达式在第二个表达式或第三个表达式之前被计算,并且只会计算第二个或第三个表达式中的一个。这在C standard 的第 6.5.15p4 节中有描述:

计算第一个操作数;有一个序列点 在其评价与第二或第三评价之间 操作数(以被评估者为准)。计算第二个操作数 仅当第一个比较不等于 0 时;第三个操作数是 仅当第一个比较等于 0 时才评估;结果是 第二个或第三个操作数的值(以 评估),转换为下面描述的类型。

就你的表达而言:

int val = (++i > ++j) ? ++i : ++j;

++i &gt; ++j 首先被评估。 ij 的增量值用于比较,因此变为 2 &gt; 3。结果是假的,所以++j 被评估而++i 不是。因此,j(即 4)的(再次)递增值随后被分配给 val

【讨论】:

    【解决方案2】:

    为时已晚,但也许有用。

    (++i > ++j) ? ++i : ++j;
    

    在文档ISO/IEC 9899:201xAnnex C(informative)Sequence points中我们发现有一个序列点

    在条件 ?: 运算符的第一个操作数的计算与第二个和第三个操作数中的任何一个计算之间

    为了明确定义行为,不得在 2 个序列点之间修改 2 次(通过副作用)同一对象。

    在您的表达式中,唯一可能出现的冲突是第一个和第二个++i++j 之间的冲突。

    在每个序列点,最后存储在对象中的值应与抽象机规定的一致(这是你在纸上计算的,就像在图灵机上一样)。

    引用5.1.2.3p3 Program execution

    在表达式 A 和 B 的求值之间存在一个序列点意味着与 A 关联的每个值计算和副作用都在与 B 关联的每个值计算和副作用之前排序。

    当您的代码中有副作用时,它们会按不同的表达式排序。规则说,在 2 个序列点之间,您可以随意排列这些表达式。

    例如。 i = i++。由于此表达式中涉及的所有运算符都不代表序列点,因此您可以根据需要置换作为副作用的表达式。 C 语言允许您使用这些序列中的任何一个

    i = i; i = i+1;i = i+1; i=i;tmp=i; i = i+1 ; i = tmp;tmp=i; i = tmp; i = i+1; 或提供与 abstract semantics of computation 相同结果的任何内容要求解释此计算。 ISO9899 标准将 C 语言定义为抽象语义。

    【讨论】:

    • 我认为将ij 的修改列举为“可能的冲突”的部分为分析增加了一些新的和有用的东西。没想到,谢谢!
    【解决方案3】:

    您的程序中可能没有UB,但在问题中: 语句int val = (++i &gt; ++j) ? ++i : ++j; 会调用未定义的行为吗?

    答案是肯定的。由于ij 已签名,因此其中一个或两个递增操作都可能溢出,在这种情况下,所有赌注都将关闭。

    当然,这在您的完整示例中不会发生,因为您已将值指定为小整数。

    【讨论】:

    • 我向你保证,问题不在于有符号整数溢出。这是关于三元运算符的第一个操作数与从第二个和第三个操作数中取胜者之间是否存在序列点。
    • 问题是关于“一些未定义的行为”,关于特定于实现的数据类型的提醒对于这样一个开放的问题是完全合适的。一个有符号整数流是UB。
    • @eckes 但问题是“是否……”,所以无条件的“是”是错误的答案。如果问题是“Can the ...”或“May the ...”,答案将是正确的。
    • 本来想抱怨的,但转念一想 +1 :)
    【解决方案4】:

    我打算对@Doug Currie 发表评论,认为有符号整数溢出是一个过于牵强的花絮,尽管 技术上正确 作为答案。恰恰相反!

    再想一想,我认为 Doug 的回答不仅是正确的,而且假设示例中的一个不完全微不足道的三行代码(但一个可能带有循环等的程序)应该扩展到一个清晰、明确的“是的”。原因如下:

    编译器看到int i = 1, j = 2;,所以它知道 ++i 将等于j,因此不可能大于j 甚至++j。现代优化器看到了这些微不足道的事情。

    当然,除非其中一个溢出。但是优化器知道这将是 UB,因此假设并根据它进行优化,它永远不会发生

    所以三元运算符的条件总是假的(在这个简单的例子中肯定是这样,但即使在循环中重复调用也是如此!),并且i 只会增加一次一次 em>,而j 将始终递增两次。因此,j 不仅总是大于i,它甚至在每次迭代中都会增加(直到溢出发生,但根据我们的假设,这永远不会发生)。

    因此,允许优化器无条件地将其转换为++i; j += 2;,这肯定不是人们所期望的。

    这同样适用于例如一个 unknown 值为ij 的循环,例如用户提供的输入。优化器可能很好地认识到操作序列仅取决于ij 的初始值。因此,可以通过复制循环(每种情况一次)并使用单个if(i&gt;j) 在两者之间切换来优化跟随有条件移动的增量序列。然后,当我们这样做时,它可能会将重复递增的循环折叠成类似(j-i)&lt;&lt;1 的东西,它只是添加了它。什么的。
    在永远不会发生溢出的假设下——这是假设优化器被允许进行,并且确实进行——这样的修改可能会完全改变程序的整个意义和操作模式完全没问题。

    尝试并调​​试它。

    【讨论】:

      猜你喜欢
      • 2021-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-01
      • 1970-01-01
      • 2019-04-19
      • 2013-01-29
      • 2010-12-11
      相关资源
      最近更新 更多