为啥这个版本的 C 中的逻辑 AND 没有显示短路行为?

Posted

技术标签:

【中文标题】为啥这个版本的 C 中的逻辑 AND 没有显示短路行为?【英文标题】:Why is this version of logical AND in C not showing short-circuit behavior?为什么这个版本的 C 中的逻辑 AND 没有显示短路行为? 【发布时间】:2014-12-08 05:11:58 【问题描述】:

是的,这是一个家庭作业问题,但我已经完成了我的研究并对该主题进行了相当多的深入思考,但无法弄清楚这一点。问题指出这段代码没有显示short-circuit behavior 并询问原因。但在我看来它确实表现出短路行为,那么有人可以解释为什么它没有吗?

在 C 中:

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

在我看来,在a 为假的情况下,程序根本不会尝试评估b,但我一定是错的。为什么程序在这种情况下甚至会触摸b,而它却没有必要?

【问题讨论】:

作为大多数人为的家庭作业的典型问题,你永远不会在生产系统中看到这种代码,除非程序员故意想变得聪明。 @RobertHarvey 您将在生产系统中看到与此完全相同的代码!它不太可能是一个名为AND() 的函数,但是函数通过值接收参数然后根据函数的逻辑评估(或不评估)它们无处不在。尽管这是一个“技巧问题”,但它是 C 的一个关键行为,需要理解。在带有点名的语言中,这个问题会有一个非常不同的答案。 @BenJackson:我评论的是代码,而不是行为。是的,您需要了解这种行为。不,你不需要写这样的代码。 如果您必须在 VB 中编写代码并遇到 IIf,这实际上是非常相关的。因为它是一个函数而不是一个运算符,所以评估不会短路。这可能会给习惯于短路运算符的开发人员带来问题,然后他们会编写IIf(x Is Nothing, Default, x.SomeValue) 之类的东西。 @alk 因为这是对教育系统的a控诉。 【参考方案1】:

这是一个棘手的问题。 bsc_and 方法的输入参数,因此将始终被评估。换句话说,sc_and(a(), b()) 将调用a() 并调用b()(不保证顺序),然​​后调用sc_and 并将a(), b() 的结果传递给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 表现出短路行为,就像普通的&amp;&amp; 一样。我根本不认为这是一个棘手的问题,因为很明显你不应该专注于三元运算符,而应该专注于 C/C++ 的函数调用机制(而且,我猜,领先很好地进入一个后续问题来编写一个短路的sc_and,这将涉及使用#define而不是函数。

您是否将三元运算符本身称为短路(或其他名称,例如“条件评估”)取决于您对短路的定义,您可以阅读各种 cmet 来了解这方面的想法。对我来说确实如此,但它与实际问题或我为什么称其为“技巧”并没有太大关系。

【讨论】:

三元算子做短路?不。它计算? 之后的表达式之一,就像在if (condition) when true else when-false 中一样。这不叫短路。 @Jens;三元运算符做短路?是的。事实上确实如此。 The short-circuit expression x Sand y (using Sand to denote the short-circuit variety) is equivalent to the conditional expression if x then y else false; the expression x Sor y is equivalent to if x then true else y.. @Jens:我认为运算符跳过对其一个或多个操作数的评估的任何情况都是一种“短路”形式,但这实际上只是您选择如何定义的问题术语。 @Jens 这是if else 的语法糖,而不是if。即便如此,也不是真的。 ?: 运算符是一个表达式if-else 是一个语句。这些在语义上是非常不同的东西,即使您可以构造一组等效的 statements 来产生与三元 expression 相同的效果。这就是为什么它被认为是短路的;大多数其他 表达式 总是计算它们的所有操作数(+,-,*,/ 等)。如果他们不这样做,那就是短路 (&amp;&amp;, ||)。 是的,对我来说重要的区别是我们谈论的是操作符。当然,流控制语句控制执行哪个代码路径。对于运算符,对于不熟悉 C 和 C 派生语言的人来说,运算符可能不会评估其所有操作数是不明显的,因此有一个术语来谈论这个属性很重要,为此,我使用“短电路”。【参考方案2】:

当声明

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

执行,如果操作数 a 评估为 false(短路行为),则不会评估 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++ 将不会递增? @ShivanDragon 这听起来像是在改变我的可观察行为。 @ShivanDragon:内联编译器不会改变行为。并不像把函数体代入那样简单。 为清楚起见,您可以添加int x = a ? b++ : 0,作为可观察到的短路。 @PaulDraper;我保留了原始的 sn-p,以免读者混淆。【参考方案3】:

正如其他人已经指出的那样,无论将什么作为两个参数传递给函数,它都会在传入时进行评估。那是在三元操作之前。

另一方面,这个

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

“短路”,因为这个并不意味着函数调用,因此不会对函数的参数进行评估。

【讨论】:

也许能解释一下原因?它在我看来与操作的 sn-p 完全相同。除了它的内联不是不同的范围。【参考方案4】:

C 三元运算符永远不会短路,因为它只计算单个表达式 a(条件),以确定表达式 b 和 c,如果可能返回任何值。

以下代码:

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,任何不评估其所有操作数的运算符都会短路。如果给出这个定义,那么三元运算符将是短路的,在这种情况下,这是我同意的原始定义。问题是术语“短路”最初用于允许这种行为的特定类型的运算符,这些是逻辑/布尔运算符,我将尝试解释的原因只有这些。

在文章Short-circuit Evaluation 之后,短路评估仅涉及在语言中实现的布尔运算符,其方式是知道第一个操作数将使第二个操作数无关紧要,因为 && 运算符是第一个操作数 false,对于 ||运算符是第一个操作数 true,the C11 spec 还在 6.5.13 逻辑与运算符和 6.5.14 逻辑或运算符中注明。

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

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

与往常一样,如果您发现有什么不对劲的地方,请在评论中提出反对意见,而不仅仅是投反对票,这只会让 SO 体验变得更糟,我相信我们可以成为一个更好的社区只是对不同意的答案投反对票。

【讨论】:

为什么投反对票?我想要 cmets,这样我就可以修复我所说的错误。请有建设性。 可能是 -1,因为它不是 100% On Topic。但无论如何我给了+1,因为你的回答至少解释了它,而且它似乎没有错ompv。 @AdrianPerez 请参阅我的回答中的 cmets 部分,了解为什么大多数人会 100% 考虑三元运算符短路。这是一个表达式,它不会根据其某些操作数的值来评估其所有操作数。那是短路。 我想不出任何不计算所有操作数的非布尔运算符。但这不是重点;它被称为短路的原因是精确的因为它是一个操作符(或者在一般意义上,一个表达式而不是一个语句),它不会评估它的所有操作数.运算符的类型并不重要。编写不短路的布尔运算符是完全可能的,您也可以编写同样如此的非布尔运算符(例如:0 的整数乘法始终为 0,因此如果第一个操作数为 0,则立即返回 0)。 x = a ? b : 0; 在语义上与(a &amp;&amp; (x = b)) || (x = 0); 相同【参考方案5】:

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

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:

因此,正如其他人正确指出的那样,问题技巧是让您专注于短路的三元运算符行为(称为“条件评估”),而不是不短路的调用参数评估(按值调用)电路。

【讨论】:

【参考方案6】:

已编辑以更正@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 程序执行

...

在抽象机中,所有表达式都按照 语义。一个实际的实现不需要评估一个 表达式,如果它可以推断出它的值没有被使用并且没有 产生了所需的副作用(包括任何由调用 函数或访问 volatile 对象)。

3.3.2.2 函数调用

....

语义

...

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

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

作为 C 标准的深水表面上的一个泼溅者,我希望能从两个方面获得适当的信息:

评估 1 / 0 是否会产生未定义的行为? 参数列表是表达式吗? (我认为不是)

附言

即使您转向 C++,并将 sc_and 定义为 inline 函数,您也将不会获得 SCE。如果您将其定义为 C 宏,就像 @alk 所做的那样,您肯定会这样做。

【讨论】:

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

以上是关于为啥这个版本的 C 中的逻辑 AND 没有显示短路行为?的主要内容,如果未能解决你的问题,请参考以下文章

HPCC-ECL 逻辑运算符 - 为啥 OR 不短路?

为啥短路不能防止与逻辑 AND (&&) 的不可达分支相关的 MissingMethodException?

c中的内部短路评估

sql语句中的逻辑短路

为啥变量没有更新?任何逻辑

有没有短路乘法之类的东西?