将递增/递减运算符放在三元/条件运算符中是不是安全?

Posted

技术标签:

【中文标题】将递增/递减运算符放在三元/条件运算符中是不是安全?【英文标题】:Is it safe to put increment/decrement operators inside ternary/conditional operators?将递增/递减运算符放在三元/条件运算符中是否安全? 【发布时间】:2014-11-01 18:31:21 【问题描述】:

这是一个例子

#include <iostream>
using namespace std;
int main()
   
    int x = 0;
    cout << (x == 0 ? x++ : x) << endl; //operator in branch
    cout << "x=" << x << endl;
    cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition
    cout << "x=" << x << endl;
    return 0;

输出:

0
x=1
1
x=1

我理解输出,但 这是未定义的行为吗?在这两种情况下都保证评估顺序吗?

即使有保证,我也很清楚使用增量/减量很快就会成为可读性问题。我只是在看到类似的代码并且立即不确定时才问,因为有很多模棱两可/未定义使用递增/递减运算符的示例,例如...

C++ 没有定义函数参数的计算顺序。 ↪

int nValue = Add(x, ++x);

C++ 语言规定您不能在序列点之间多次修改变量。 ↪

 x = ++y + y++

由于递增和递减运算符具有副作用,因此在预处理器宏中使用带有递增或递减运算符的表达式可能会产生不良结果。 ↪

 #define max(a,b) ((a)<(b))?(b):(a)
 k = max( ++i, j );

【问题讨论】:

相关(不重复):***.com/questions/10995445/… - 描述分配回递增变量的特殊情况。 注意:定义明确只是一个问题。可维护是另一个。如果你不得不问我们,下一个阅读该代码的人如何确保它是安全的? “真正的作家重写以避免问题。” main() 的第 4 行中的减量运算符在此示例中无关紧要,因为 || 的短路行为将导致 --x 被完全跳过。 @JLRishe 实际上问题在于它:是否保证短路以使 --x 永远不会被评估? (下面回答) @jozxyqk 好的,但在这种情况下,它与三元/条件运算符无关,与|| 运算符有关。 x == 1 || --x == 0 在条件运算符参与之前已被完全评估,到那时,--x 将被跳过。换句话说,第 4 行没有告诉我们任何关于条件运算符的重要信息。 【参考方案1】:

对于条件运算符(§5.16 [expr.cond]/p1):

与第一个相关的每个值计算和副作用 表达式在每个值计算和副作用之前排序 与第二个或第三个表达式相关联。

对于逻辑 OR 运算符(§5.15 [expr.log.or]/p1-2):

如果第一个操作数的计算结果为true,则不计算第二个操作数。 [...]如果计算第二个表达式,则每个值计算和 与第一个表达式相关的副作用在之前排序 与第二个相关的每个值计算和副作用 表达。

您的代码的行为是明确定义的。

【讨论】:

【参考方案2】:

我理解输出,但这是否是未定义的行为?

代码被完美定义。 C11 标准说:

6.5.15 条件运算符

计算第一个操作数; 在它的求值和 评估第二个或第三个操作数(以评估者为准)。第二个操作数 仅当第一个比较不等于 0 时才评估;仅在以下情况下评估第三个操作数 第一个比较等于0;结果是第二个或第三个操作数的值 (以评估为准),转换为下面描述的类型。110)

6.5.14 逻辑或运算符

与按位 | 运算符不同,|| 运算符保证从左到右的评估;如果 第二个操作数被求值,第一个求值之间有一个序列点 和第二个操作数。如果第一个操作数与0 比较不相等,则第二个操作数是 不评价。

进一步wiki举例说明:

&amp;&amp;(逻辑与)、||(逻辑或)(作为短路评估的一部分)和comma operators 的左右操作数的求值之间。例如,在表达式*p++ != 0 &amp;&amp; *q++ != 0 中,子表达式*p++ != 0 的所有副作用都在尝试访问q 之前完成。

在三元“问号”运算符的第一个操作数与第二个或第三个操作数的求值之间。例如,在表达式a = (*p++) ? (*p++) : 0 中,在第一个*p++ 之后有一个序列点,这意味着在执行第二个实例时它已经递增。

||?: 的规则对于 C++(第 5.15 和 5.16 节)与 C 中的规则相同。


在这两种情况下都可以保证评估顺序吗?

是的。运算符||&amp;&amp;,?: 的操作数的求值顺序保证从左到右。

【讨论】:

@Slava;哦!它是如何误导的?你能解释一下吗? 代码中有2个例子,第二个有UB imo。我可能是错的,但答案无论如何都不会回答,因为它基于布尔运算符是否有序列点。 @Slava;哪个有UB? @hacks 没有 UB 我错了。但是您的回答没有提到为什么第二个示例没有 UB,即布尔运算符 || 中有一个序列点【参考方案3】:

在 C 中,一个对象的存储值只能在两个序列点之间修改一次。

出现一个序列点:

    完整表达结束。 &amp;&amp;||?: 运算符 在函数调用中。

例如,这个表达式x = i++ * i++ 未定义,而x = i++ &amp;&amp; i++ 完全合法

您的代码显示定义的行为

int x=0;

cout

在上面的表达式中x0,所以x++会被执行,这里x++是后自增所以它会输出0

cout

在上面的表达式中,x 现在的值是 1,所以输出将是 1

cout

这里x1,因此不评估下一个条件(--x == 0),输出将为1

cout

由于表达式 --x == 0 未计算,输出将再次为 1

【讨论】:

【参考方案4】:

是的,使用递增/递减运算符是安全的。以下是您的代码中发生的情况:

片段 #1

cout << (x == 0 ? x++ : x) << endl; //operator in branch

在这个 sn-p 中,您正在测试 x == 0,即 true。因为它是true,所以您的三元表达式计算x++。 由于您在此处使用后增量,x 的原始值被打印到标准输出流,然后 x 增加。

片段 #2

cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition

这个 sn-p 有点令人困惑,但它仍然会产生可预测的结果。此时,x = 1 来自第一个 sn-p。在三元表达式中,先计算条件部分;但是,由于Short-Circuiting,第二个条件 --x == 0 永远不会被评估。

对于 C++,运算符 ||&amp;&amp; 分别是 逻辑 OR逻辑 AND 的短路布尔运算符。当您使用这些运算符时,会检查您的条件(从左到右),直到可以确定最终结果。确定结果后,不再检查任何条件。

查看 sn-p #2,您的第一个条件检查是否为 x == 1。由于您的第一个条件评估为 true 并且您使用的是逻辑 OR,因此无需继续评估其他条件。这意味着--x == 0从未被执行


关于短路的简要说明:

短路对于提高程序的性能很有用。 假设您有这样的条件,它调用了几个耗时的函数:

if (task1() && task2())
 
    //...Do something...

在此示例中,除非task1 成功完成,否则永远不应调用task2task2 依赖于由task1 更改的某些数据)。

因为我们使用了短路 AND 运算符,如果 task1 失败并返回 false,则 if 语句有足够的信息提前退出并停止检查其他条件。这意味着永远不会调用task2

【讨论】:

【参考方案5】:

在三元运算符和布尔&amp;&amp;|| 运算中有保证的执行顺序,因此计算顺序点不存在冲突。

一次一个

 cout << (x == 0 ? x++ : x) << endl; //operator in branch

将始终输出x,但仅当它为 0 时才会递增。

 cout << (x == 1 || --x == 0 ? 1 : 2) << endl; //operator in condition

这也是很好的定义,如果x 为 1,它不会评估 RHS,如果不是,它会递减它,但--x 永远不会为 0,所以如果 x==1 它将是真的,在这种情况下,x 现在也将为 0。

在后一种情况下,如果 x 为 INT_MIN,则将其递减不是明确定义的行为(它会执行)。

如果 x 是 INT_MAX,x 不会为 0,则不会发生这种情况,因此您是安全的。

【讨论】:

边缘情况,但是如果x在执行时是INT_MIN,第二个代码sn-p当然会有UB。 INT_MAX 的等效问题不会影响第一个 sn-p。

以上是关于将递增/递减运算符放在三元/条件运算符中是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

在C#三元运算符给出错误:只有赋值,调用,递增,递减和新对象表达式可用作语句

递增和递减运算符

js基础概念-操作符

在啥情况下(如果有),C 中前缀和后缀递增/递减运算符之间的性能是不是存在差异? [复制]

Javascript中的递增/递减运算符是不是被认为是错误的形式? [复制]

递增递减运算符