为什么带逗号的三元运算符只在真值情况下计算一个表达式?

120

我正在使用C++ Primer这本书学习C++,其中的一个练习是:

解释以下表达式的含义:someValue ? ++x, ++y : --x, --y

我们知道,三目运算符的优先级高于逗号运算符。对于二元运算符,这相当容易理解,但对于三目运算符,我有些困惑。对于二元运算符,“优先级更高”意味着我们可以在具有更高优先级的表达式周围使用括号,并且它不会改变执行顺序。

对于三目运算符,我会这样做:

(someValue ? ++x, ++y : --x, --y)

实际上,这导致的代码效果相同,但这并没有帮助我理解编译器将如何分组代码。

然而,通过使用C++编译器进行测试,我知道该表达式可以编译,并且我不知道单独的:运算符代表什么。因此,编译器似乎正确解释了三元运算符。

然后我以两种方式执行了程序:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

结果为:

11 10

而另一方面,当someValue = false时,它会打印:

9 9

为什么C++编译器生成的代码在三元运算符的真分支中只增加 x,而在假分支中减少了 xy

我甚至已经在真分支周围加上括号,就像这样:

someValue ? (++x, ++y) : --x, --y;

但结果仍为11 10


5
"Precedence"只是C++中的一种 emergent phenomenon。直接查看语言语法并了解表达式的工作方式可能更简单。 - Kerrek SB
26
我们并不是非常关心原则。 :-) 你必须在这里问这个问题,表明代码永远不会通过你的同行程序员的代码审查。这使得对于这个实际上如何工作的知识变得不太有用。当然,除非你想参加“混淆C代码竞赛”。(http://www.ioccc.org/) - Bo Persson
5
没有这样的例子供未来的审核人员学习,他们就不会知道为什么要拒绝此产品。 - Alex Celeste
8
警钟应该已经在敲响了。同一语句中存在多个增量和减量操作(叮叮叮!)。你可以使用if-else条件语句,却使用了三目运算符(叮叮叮!)。等等,那些逗号是可怕的逗号运算符吗?(叮叮,叮叮,叮叮!)由于所有这些操作符,可能会有一些优先级问题。因此,我们永远无法使用它。那为什么要浪费时间去弄清楚它是否起作用? - Bo Persson
4
小问题:?的名称是条件运算符。术语三元运算符仅表示具有三个操作数的运算符。条件运算符是三元运算符的一个例子,但一种语言理论上可以有多个三元运算符。 - bta
显示剩余16条评论
5个回答

126

正如@Rakete在他们出色的回答中所说,这很棘手。我想再补充一点。

三元运算符必须具有以下形式:

逻辑或表达式 ? 表达式 : 赋值表达式

因此我们有以下映射:

  • someValue逻辑或表达式
  • ++x, ++y表达式
  • ???是赋值表达式--x,--y还是仅限于--x

实际上仅限于--x,因为按照C++的语法规则,一个赋值表达式不能被解析为用逗号隔开的两个表达式(assignment-expression),因此--x,--y不能被视为一个赋值表达式

这导致三元(条件)表达式部分看起来像这样:

someValue?++x,++y:--x

为了提高可读性,可以将++x,++y视为计算as-if带括号的(++x, ++y); 介于?:之间的任何内容都将在条件语句之后顺序执行 (我将在本文其余部分中使用括号)。

并按照以下顺序进行评估:

  1. someValue?
  2. (++x, ++y)--x(取决于1的bool结果)

然后,将此表达式作为逗号运算符的左子表达式处理,右子表达式为--y,如下所示:

(someValue?(++x,++y):--x), --y;
这意味着左侧是一个"被丢弃的-值表达式",这意味着它一定会被评估,但然后我们评估右侧并返回它。
那么当someValue为true时会发生什么?
1. (someValue? (++x, ++y): --x)执行并将x和y增加为11。 2. 左侧表达式被丢弃(尽管增量的副作用仍然存在) 3. 我们评估逗号运算符的右侧:--y,然后将y减少到10。
要“修复”此行为,您可以将--x,--y与括号分组,将其转换为“主表达式”,该表达式是一个有效的“赋值表达式”的条目。
someValue?++x,++y:(--x, --y);
assignment-expression连接到primary-expression的长链包括:conditional-expressionlogical-or-expressionlogical-and-expressioninclusive-or-expressionexclusive-or-expressionand-expressionequality-expressionrelational-expressionshift-expressionadditive-expressionmultiplicative-expressionpm-expressioncast-expressionunary-expressionpostfix-expression

10
感谢您花费精力解开语法规则;这样做表明C++的语法比大多数教科书中所述的要更加复杂。 - sdenham
4
当人们询问为什么“表达式导向语言”很好(即当 { ... } 可以被视为表达式时),我现在有一个答案 => 那就是避免引入逗号运算符,因为它的行为非常棘手。 - Matthieu M.
你能给我一个链接,让我了解“赋值表达式链”吗? - MiP
@MiP:我是从标准本身中提取的,你可以在gram.expr下找到它。 - AndyG

90

哇,这很棘手。

编译器将您的表达式视为:

(someValue ? (++x, ++y) : --x), --y;
三元运算符需要有一个 :,它不能单独存在于那个上下文中,但在它之后,没有理由逗号应该属于 false 情况。
现在你可能更明白为什么会得到这个输出了。如果 someValue 为 true,那么 ++x++y--y 将被执行,这并不会有效地改变 y,但会将 x 加一。
如果 someValue 为 false,则执行 --x--y,将它们两个都减一。

43
为什么C++编译器会生成代码,使得三目运算符的真分支只增加x?
你误解了发生的事情。真分支同时递增x和y。然而,在此之后,无条件地将y递减。
这是它发生的方式:由于在C++中条件运算符的优先级高于逗号运算符,所以编译器按照以下方式解析表达式:
   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

请注意逗号后的“孤儿”--y。 这就是导致最初已经增加的y被减少的原因。

我甚至进一步将真分支放在括号中,像这样:

someValue ? (++x, ++y) : --x, --y;
你走在正确的道路上,但是你把错误的分支放在了括号里:你可以通过在else分支加括号来解决这个问题,像这样:

someValue ? ++x, ++y : (--x, --y);

演示(打印11 11)


5

您的问题在于三目运算符并不比逗号运算符优先级更高。实际上,C++无法仅通过优先级准确描述-而正是三目运算符和逗号之间的交互导致了它的失败。

a ? b++, c++ : d++

被视为:

a ? (b++, c++) : d++

(逗号的表现就像它具有更高的优先级)。另一方面,
a ? b++ : c++, d++

被视为:
(a ? b++ : c++), d++

三元运算符的优先级较高。

1
我认为这仍然属于优先级范畴,因为中间行只有一种有效的解析方式,对吧?不过这仍然是一个有用的例子。 - sudo rm -rf slash

3

在回答中被忽视(但在评论中提到)的一点是条件运算符通常被用作将两个值之一赋给变量的快捷方式,这可能是其设计意图所在。

因此,更大的背景是:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

这种情况本身就很荒谬,所以问题是多方面的:

  • 这种语言允许在赋值中产生荒谬的副作用。
  • 编译器没有警告你正在做一些奇怪的事情。
  • 这本书似乎着重于“技巧”问题。我们只能希望背后的答案是:“这个表达式的行为取决于一个刻意设计的边缘案例,产生了没有人预料到的副作用。永远不要这样做。”

1
赋值其中一个变量是三元运算符的常见用法,但有时候拥有 if 的表达式形式也很有用(例如,在 for 循环中的增量表达式)。更大的上下文可能是 for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y)),其中循环可以独立地修改 xy - Martin Bonner supports Monica
@MartinBonner 我还不太确定,而且这个例子似乎很好地证明了bo-perrson的观点,正如他引用了Tony Hoare所说的话。 - Taryn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接