为什么C语言中的逻辑AND运算符版本没有显示短路行为?

80

是的,这是一个作业问题,但我已经做了研究并深思熟虑了一段时间,但仍然无法解决。问题陈述说,这段代码不表现出短路行为,并问为什么。但在我看来,它确实表现出了短路行为,所以有人能解释一下为什么它没有吗?

在C中:

int sc_and(int a, int b) {
    return a ? b : 0;
}

在我的看法中,如果a为假,程序将不会尝试评估b,但我一定是错的。为什么程序在这种情况下甚至要触及b,而它并不必须这么做?


11
大多数教学作业问题都是刻意编造的,这种代码在实际生产系统中几乎不会出现,除非程序员刻意想要显示自己的聪明才智。 - Robert Harvey
49
@RobertHarvey,你在生产系统中经常会看到类似这样的代码!虽然不太可能是一个名为AND()的函数,但是通过值接收参数并根据函数逻辑计算(或不计算)的函数随处可见。尽管这是一个“恶作剧问题”,但这是理解C语言的关键行为。在一个采用按名称调用的语言中,这个问题的答案会有很大不同。 - Ben Jackson
7
@BenJackson:我是在评论代码,而不是行为。是的,你需要理解行为。不,你不需要编写这样的代码。 - Robert Harvey
3
如果你曾经需要在VB中编写代码并遇到IIf,那么这实际上是非常相关的。因为它是一个函数而不是运算符,所以评估不会被短路。这可能会给开发人员带来问题,他们习惯于短路运算符,然后编写像IIf(x Is Nothing, Default, x.SomeValue)这样的代码。 - Dan Bryant
3
@alk因为这是对教育系统的控诉。 - Shivan Dragon
显示剩余7条评论
6个回答

118

这是一个有技巧性的问题。 bsc_and 方法的输入参数,因此将始终被评估。换句话说,sc_and(a(), b()) 将调用 a() 和调用 b()(顺序不保证),然后使用 a(), b() 的结果调用 sc_and,该结果传递给 a?b:0。这与三元运算符本身无关,它绝对会短路。

更新

关于为什么我称这个问题为“有技巧性的问题”:这是因为缺乏明确定义的上下文来考虑“短路”(至少是由 OP 复制的)。许多人在只给出函数定义时,假设问题的上下文是询问函数的主体;他们通常不将函数视为一个表达式本身。这就是问题的“技巧”所在;提醒您,在编程中,特别是在像 C 一样经常有许多例外规则的语言中,您不能这样做。例如,如果问题是这样问的:

请考虑以下代码。当从main调用sc_and时,它会表现出短路行为吗?
int sc_and(int a, int b){
    return a?b:0;
}

int a(){
    cout<<"called a!"<<endl;
    return 0;
}

int b(){
    cout<<"called b!"<<endl;
    return 1;
}

int main(char* argc, char** argv){
    int x = sc_and(a(), b());
    return 0;
}

如果你把sc_and看作是你自己领域特定语言中的一个运算符,并评估调用sc_and是否表现出类似于常规&&的短路行为,那么这将立即变得清晰明了。我不认为这是一个诡计问题,因为很明显你不应该专注于三元运算符,而应该专注于C/C++的函数调用机制(我猜测,这会很好地引出一个后续问题,要求编写一个执行短路的sc_and,这将涉及使用#define而不是函数)。

无论你是否称三元运算符本身所做的事情为短路(或其他什么,比如“条件评估”),这取决于你对短路的定义,你可以阅读各种评论来了解思考。在我的定义中,它确实是短路,但这与实际问题或我称其为“诡计”的原因并不相关。


13
三目运算符进行短路操作吗?不是的。它会评估跟在问号后面的其中一个表达式,就像 if (condition) {当条件为真} else {当条件为假} 一样。这并不被称为短路操作。 - Jens
12
@Jens; 三元运算符可以短路吗?是的。实际上它可以。使用短路方式的表达式x Sand y(使用Sand表示短路变量)等价于条件表达式if x then y else false;,表达式x Sor y等价于if x then true else y。(参见http://en.wikipedia.org/wiki/Short-circuit_evaluation)。 - haccks
17
@Jens:我会称任何一个运算符跳过对其一个或多个操作数的评估为“短路”,但这实际上只是你选择如何定义该术语的问题。 - R.. GitHub STOP HELPING ICE
13
@Jens 这是一个对“if else”的语法糖,而不是单独的“if”。即使这样,它也不完全是;?:操作符是一个表达式,“if-else”是一个语句。虽然你可以构造一组等效的语句来产生与三元表达式相同的效果,但在语义上它们是非常不同的事情。这就是为什么它被认为是短路的原因;大多数其他表达式总是评估它们所有的操作数(+,-,*,/等)。当它们不这样做时,便是短路(&&, ||)。 - aruisdante
9
对于我来说,重要的区别在于我们正在谈论运算符。当然,流程控制语句 控制哪些代码路径被执行。对于运算符,对于不熟悉C和C衍生语言的人来说,一个运算符可能不会评估其所有操作数,因此有必要使用一个术语来谈论这种属性,而我使用“短路”来表达这个意思。 - R.. GitHub STOP HELPING ICE
显示剩余11条评论

43

当这个陈述被执行时

bool x = a && b++;  // a and b are of int type

执行时,如果操作数 afalse(短路行为),则不会评估 b++。这意味着对 b 的副作用不会发生。

现在看看这个函数:

bool and_fun(int a, int b)
{
     return a && b; 
}

并称之为

bool x = and_fun(a, b++);
在这种情况下,无论 atrue 还是 falseb++ 都将在函数调用期间始终被评估1,并且 b 的副作用将始终发生。
同样适用于:
int x = a ? b : 0; // Short circuit behavior 

int sc_and (int a, int b) // No short circuit behavior.
{
   return a ? b : 0;
} 

函数参数的求值顺序是未指定的。


1
好的,与Stephen Quan的答案相协调:编译器是否有可能(合法地)将“and_fun”函数内联,以便当您调用“bool x = and_fun(a, b ++);”时,如果a为真,则b ++不会被递增? - Shivan Dragon
4
听起来像是改变可观察行为。 - sapi
3
当进行内联编译时,编译器不会改变行为。它不像简单地用函数体替换。 - Jack Aidley
为了清楚起见,您可以添加 int x = a ? b++ : 0 作为可观察的短路。 - Paul Draper
@PaulDraper:我保留了原始代码片段,以便读者不会混淆。 - haccks

19

正如其他人所指出的,无论作为两个参数传递给函数的是什么内容,它都会按照传递时的形式进行评估,这发生在三元操作之前。

另一方面,这个...

#define sc_and(a, b) \
  ((a) ?(b) :0)
would会“短路”,因为这个并不意味着一个函数调用,因此不会对函数的参数进行求值。

也许可以解释一下为什么吗?对我来说,它看起来与 OP 的代码片段完全相同,只是它是内联的而不是在不同的作用域中。 - dhein

5

已编辑以纠正@cmasters评论中指出的错误。


int sc_and(int a, int b) {
    return a ? b : 0;
}

...return表达式确实展示了短路求值,但函数调用并没有。

尝试调用

sc_and (0, 1 / 0);

该函数调用计算了1/0,尽管它从未被使用,因此可能会导致除以零的错误。
相关摘录来自(草案)ANSI C标准:
2.1.2.3程序执行
...
在抽象机器中,所有表达式都按语义规定进行评估。如果实际实现可以推断出其值未被使用,并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的任何副作用),则无需评估表达式的一部分。

3.3.2.2函数调用
...
语义
...
在准备调用函数时,将评估参数,并将每个参数赋予相应参数的值。
我的猜测是每个参数都被评估为一个表达式,但参数列表作为整体不是一个表达式,因此非SCE行为是强制性的。
作为 C 标准深水区的涟漪者,我希望能够得到两个方面的正确信息:
  • 求值 1 / 0 是否会产生未定义行为?
  • 参数列表是否为表达式?(我认为不是)

P.S.

即使您转移到C ++并将sc_and定义为inline函数,您也不会获得SCE。如果您像@alk那样将其定义为C宏,那么肯定会获得。


1
不,inline函数不会改变函数调用的语义。而这些语义指定所有参数被评估,正如您正确引用的那样。即使编译器可以优化调用,可见行为也不能改变,即sc_and(f(),g())必须表现得好像f()g()总是被调用。sc_and(0,1/0)是一个糟糕的例子,因为它未定义的行为,编译器甚至不需要调用sc_and()... - cmaster - reinstate monica
@cmaster 谢谢您。我之前并不知道 C++ 的 inline 保留了调用语义。我选择了零除,因为它符合示例,并且我认为 SCE 经常被利用来避免未定义的行为。 - Thumbnail

4
为了清晰地看到三元操作符的短路,请稍微更改代码,使用函数指针而不是整数:
```html

要清楚地看到三元运算符的短路,请稍微更改代码,使用函数指针而不是整数:

```
int a() {
    printf("I'm a() returning 0\n");
    return 0;
}

int b() {
    printf("And I'm b() returning 1 (not that it matters)\n");
    return 1;
}

int sc_and(int (*a)(), int (*b)()) {
    a() ? b() : 0;
}

int main() {
    sc_and(a, b);
    return 0;
}

然后将它编译(即使几乎没有任何优化:-O0!)。您会看到如果a()返回false,则不会执行b()

% gcc -O0 tershort.c            
% ./a.out 
I'm a() returning 0
% 

这里生成的汇编代码如下:

    call    *%rdx      <-- call a()
    testl   %eax, %eax <-- test result
    je      .L8        <-- skip if 0 (false)
    movq    -16(%rbp), %rdx
    movl    $0, %eax
    call    *%rdx      <- calls b() only if not skipped
.L8:

正如其他人正确指出的那样,这个问题的技巧在于让您关注三元运算符的行为,它会进行条件评估并且具有短路功能 (称之为'条件评估'),而不是调用时的参数评估 (传值调用),它没有短路功能。


0
C三目运算符永远不会短路,因为它只评估单个表达式a(条件),以确定由表达式bc给出的值,如果可能返回任何值。
以下代码:
int ret = a ? b : c; // Here, b and c are expressions that return a value.

这几乎等同于以下代码:

int ret;
if(a) {ret = b} else {ret = c}

表达式a可以由其他运算符(如&&或||)组成,这些运算符可能会短路,因为它们在返回值之前可能会评估两个表达式,但这不会被视为三元运算符进行短路,而是像常规if语句中使用的条件运算符。

更新:

有关三元运算符是否为短路运算符存在一些争议。该论点认为,根据下面评论中@aruisdante的说法,任何不评估其所有操作数的运算符都会进行短路。如果给定此定义,则三元运算符将进行短路,在这种情况下,如果这是最初的定义,我同意。问题在于,“短路”一词最初用于允许此行为的特定类型的运算符,这些运算符是逻辑/布尔运算符,而仅限于这些运算符的原因是我将尝试解释的原因。

根据短路求值文章,短路求值仅适用于语言中实现的布尔运算符,其中第一个操作数确定后,第二个操作数将变得无关紧要。对于第一个操作数为false的&&运算符和第一个操作数为true的||运算符,C11规范也在6.5.13逻辑AND运算符和6.5.14逻辑OR运算符中进行了说明。
这意味着要识别短路行为,您应该期望在必须像布尔运算符一样评估所有操作数的运算符中找到它,如果第一个操作数不使第二个操作数变得无关紧要。这与MathWorks在“逻辑短路”部分下另一个短路定义所写的内容一致,因为短路来自逻辑运算符。

正如我一直试图解释的C三元运算符(也称为三元if),只评估两个操作数,它评估第一个操作数,然后评估第二个操作数,取决于第一个操作数的值是两个剩余值之一。它始终这样做,在任何情况下都不应该评估所有三个运算数,因此在任何情况下都没有“短路”。

一如既往,如果您发现有什么不对的地方,请撰写一条评论并提出反对意见,而不仅仅是点个踩,这只会使SO的体验更糟糕,我相信我们可以成为一个更好的社区,而不是只是不同意答案就点踩的社区。


3
为什么会有负投票呢?我希望能得到评论,这样我才能修正自己说错的地方。请提出建设性意见。 - Adrián Pérez
2
也许不是100%相关,但无论如何我还是给了+1,因为你的回答至少解释了这个语句,并且似乎没有错。 - dhein
4
请参见我回答中的评论部分,了解为什么大多数人会完全将三元运算符视为短路。它是一种表达式,根据其某些操作数的值不评估所有操作数。这就是短路。 - aruisdante
3
我想不出任何非布尔运算符不会评估其所有操作数,但这并不是重点;之所以称其为短路计算,正是因为它是一种运算符(或者在一般意义上说,是一个表达式而不是语句),不评估其所有操作数。运算符的类型并不真正重要。完全可以编写不短路的布尔运算符,也可以编写短路的非布尔运算符(例如:整数乘法中若第一个操作数为0,则始终为0,因此立即返回0)。 - aruisdante
2
x = a ? b : 0;在语义上等同于(a && (x = b)) || (x = 0); - jxh
显示剩余10条评论

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