为什么C中的此逻辑AND版本没有显示短路行为?


80

是的,这是一个家庭作业问题,但是我已经完成了我的研究并且对这个主题进行了大量的深入思考,无法解决。问题指出,这段代码没有表现出短路行为,并询问原因。但是在我看来,它确实表现出短路行为,所以有人可以解释为什么不这样做吗?

在C中:

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

在我看来,在a错误的情况下,该程序根本不会尝试评估b,但我一定是错的。b在这种情况下,为什么程序在不需要的情况下甚至可以触摸?


11
就像大多数人为作业问题一样,除非程序员刻意尝试变得聪明,否则您将永远不会在生产系统中看到这种代码。
罗伯特·哈维

49
@RobertHarvey您将始终在生产系统中看到完全像这样的代码!它不可能是一个名为的函数AND(),但是到处都是按值接收参数然后根据函数的逻辑对参数进行评估(或不评估)的函数。尽管是一个“技巧性问题”,但理解它仍然是C的关键行为。用按名字呼叫的语言,这个问题会有完全不同的答案。
本杰克逊

7
@BenJackson:我在评论代码,而不是行为。是的,您需要了解行为。不,您不需要编写这样的代码。
罗伯特·哈维

3
如果您不得不在VB中进行编码并遇到IIf,那么这实际上是非常相关的。因为它是功能而不是运算符,所以评估不会短路。这可能会给习惯于短路操作员的开发人员带来问题,他们随后会编写IIf(x Is Nothing, Default, x.SomeValue)
丹·布莱恩特

3
@alk,因为这是对教育系统起诉。
Shivan Dragon 2014年

Answers:


118

这是一个技巧问题。b是方法的输入参数sc_and,因此将始终进行评估。换句话说,sc_and(a(), b())将呼叫a()和呼叫b()(不能保证顺序),然​​后将呼叫sc_and的结果a(), b()传递到a?b:0。它与三元运算符本身无关,后者绝对会短路。

更新

关于我为什么将其称为“技巧性问题”的原因:这是因为缺乏在何处考虑“短路”的明确定义的上下文(至少由OP复制)。许多人,仅给出函数定义时,就认为问题的上下文是在询问函数的主体。他们常常不认为函数本身就是一种表达。这是问题的“窍门”。提醒您,在一般编程中,尤其是在C语言之类的语言中,规则通常有很多例外,您不能这样做。例如,如果这样问问题:

考虑下面的代码。从main调用时,将sc_and exibit短路行为:

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确实短路的问题,这将涉及使用a#define而非函数)。

是否调用三元运算符本身会造成短路(或其他诸如“条件评估”)的操作,取决于您对短路的定义,您可以阅读各种注释以获取相关的想法。从我的角度来看确实如此,但它与实际问题或我为何称其为“技巧”并不十分相关。


13
三元运算符短循环吗?不会。它会评估后面的表达式之一?,就像中的if (condition) {when true} else {when-false}。这不称为短路。
延斯2014年


17
@Jens:在任何情况下,如果操作员跳过对一个或多个操作数的求值,都将其称为“短路”,但这实际上只是选择定义术语的方式。
R .. GitHub停止帮助ICE

13
@Jens是if else“而不是”的语法糖if。即使这样,它并不是真的。该?:运营商是表达if-else是一个声明。即使您可以构造与三元表达式产生相同效果的等效语句集,这些语句在语义上也大不相同。这就是为什么它被认为是短路的原因。其他大多数表达式始终求值其所有操作数(等)。如果没有,则表示短路()。+,-,*,/&&, ||
2014年

9
是的,对我来说,重要的区别是我们在谈论运营商。当然,流控制语句控制执行哪个代码路径。对于运算符,对于不熟悉C和C派生语言的人来说,运算符可能不会评估其所有操作数并不明显,因此,重要的是要有一个术语来讨论此属性,为此,我使用“电路”。
R .. GitHub停止帮助ICE

43

当陈述

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

执行,b++如果操作数的a计算结果为false(短路行为),则不会进行评估。这意味着b不会发生副作用。

现在,看一下函数:

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

并称之为

bool x = and_fun(a, b++);

在这种情况下,在函数调用期间,将始终将aistrue或评估为1,并且始终会发生副作用。falseb++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未指定函数参数的评估顺序。


1
好的,因此可以与Stephen Quan的回答相佐证:可能(合法)使Compilar内联“ and_fun”函数,这样当您调用“ bool x = and_fun(a,b ++);”时就可以了。如果a为true,b ++不会递增?
Shivan Dragon

4
@ShivanDragon听起​​来像是改变了我的可观察行为。
sapi 2014年

3
@ShivanDragon:内联时,编译器不会更改行为。它不只是替换功能主体
而已

为了清楚起见,您可以添加int x = a ? b++ : 0,作为可观察到的短路。
Paul Draper 2014年

@PaulDraper; 我保留了原始代码段,以使读者不要混淆。
c

19

正如其他人已经指出的那样,无论将什么作为两个参数传递给函数,都将在传递参数时对其求值。这是在进行整数运算之前。

另一方面,这

#define sc_and(a, b) \
  ((a) ?(b) :0)

“短路”,因为此并不意味着函数调用,并且因此不执行函数自变量的求值。


也许解释为什么?在我看来,它与op的片段完全相同。除了它的内联注释器以外,还有其他作用域。
dhein 2014年

5

编辑以更正@cmasters注释中指出的错误。


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

... returned表达式确实具有短路评估功能,但函数调用没有。

尝试致电

sc_and (0, 1 / 0);

尽管函数调用1 / 0从未使用过,但它的求值结果为-可能是-除零错误。

ANSI C标准草案的相关摘录是:

2.1.2.3程序执行

...

在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。

3.3.2.2函数调用

....

语义学

...

在准备调用函数时,将评估参数,并为每个参数分配相应参数的值。

我的猜测是,每个参数都作为一个表达式求值,但整个参数列表不是一个表达式,因此非SCE行为是强制性的。

作为C标准深水表面的溅水工具,我希望从两个方面获得正确的见解:

  • 评估会1 / 0产生不确定的行为吗?
  • 参数列表是表达式吗?(我想不是)

聚苯乙烯

即使您使用C ++并定义sc_andinline函数,也不会获得SCE。如果将其定义为C宏(如@alk一样),当然可以。


1
不可以,inline函数不会改变函数调用的语义。正如您正确引用的那样,这些语义指定对所有参数进行求值。即使编译器可以优化调用,可见的行为也不得更改,即sc_and(f(), g())必须表现得好像两者都一样f()并且g()始终被调用。sc_and(0, 1/0)这是一个不好的例子,因为它未定义的行为,甚至不需要编译器调用sc_and()...
cmaster-恢复monica 2014年

@cmaster谢谢。我只是不知道C ++inline保留了调用语义。我坚持零除法,因为它适合示例,我认为经常利用SCE来避免不确定的行为。
缩略图

4

为了清楚地看到三元运算短路,请尝试稍微更改代码以使用函数指针而不是整数:

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!)。b()如果a()返回false,您将看到未执行。

% 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,对于|| 运算符是第一个操作数trueC11规范还在6.5.13逻辑AND运算符和6.5.14逻辑OR运算符中进行了注明。

这意味着要识别短路行为,如果第一个操作数与第二个操作数无关,那么您将期望在必须评估所有操作数的运算符中进行识别,就像布尔运算符一样。这与MathWorks中“逻辑短路”部分下关于短路的另一种定义所写的内容一致,因为短路来自逻辑运算符。

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

与往常一样,如果您发现某些问题是不对的,请发表评论,并对此提出反对意见,而不仅要讨价还价,这只会使SO体验变得更糟,我相信我们可以成为一个更好的社区,而那些人只需对问题进行投票就可以了一个不同意。


3
以及为什么要投票?我想发表评论,以便纠正我说错的地方。请具有建设性。
阿德里安·佩雷斯

2
可能是-1,因为它不是100%主题。但无论如何,我还是给+1,因为您的回答至少说明了这个问题,而且ompv似乎并没有错。
dhein 2014年

4
@AdrianPerez在我的答案中查看评论部分,以了解为什么大多数人会100%考虑让三级操作员担当重任。该表达式不会根据某些操作数的值来评估其所有操作数。那是短路。
2014年

3
我想不出任何不评估其所有操作数的非布尔运算符。但这不是重点。之所以将其称为“短路”是因为precisley,因为它是一个运算符(或一般而言,是一个表达式,而不是一条语句),并不评估其所有操作数。运算符的类型并不重要。编写没有短路的布尔运算符是完全有可能的,您也可以编写也有短路运算符的非布尔运算符(例如:整数倍数始终为0,因此如果第一个操作数为0,则立即返回0)。
2014年

2
x = a ? b : 0;在语义上与(a && (x = b)) || (x = 0);
jxh 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.