为什么这个版本的逻辑和在 C 没有显示短路行为?

是的,这是一个家庭作业问题,但我已经做了我的研究和相当数量的深入思考的主题,无法弄清楚这一点。问题指出这段代码没有展示 短路行为短路行为,并问为什么。但在我看来,它确实表现出了短路行为,谁能解释一下为什么没有?

C:

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

在我看来,在 a为 false 的情况下,程序根本不会尝试计算 b,但我一定是错的。为什么在这种情况下程序甚至触摸 b,当它不必?

6431 次浏览

这是个陷阱问题。bsc_and方法的输入参数,因此将始终进行计算。换句话说,sc_and(a(), b())将调用 a()并调用 b()(不保证订单) ,然后调用 sc_and并将 a(), b()的结果传递给 a?b:0。它与三元运算符本身没有任何关系,因为三元运算符绝对会短路。

更新

关于为什么我称之为“陷阱问题”: 这是因为缺乏明确的上下文来考虑“短路”(至少作为 OP 的转载)。许多人,当给定一个函数定义,假设问题的上下文询问函数的 < em > body ; 他们通常不认为函数本身就是一个表达式。这是这个问题的“诀窍”; 为了提醒你,在编程中,特别是在像 C-like 这样的语言中,通常有许多规则的例外,你不能这样做。例如,如果问题是这样的:

考虑下面的代码。当从 总台调用 sc _ 和会出现短路行为:

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看作是你自己的 领域特定语言中的一个操作符,并且评估 对 ABC0的呼叫表现出短路行为,就像正常的 &&一样。我不认为这是一个陷阱问题,因为很明显,你不应该关注三元操作符,而应该关注 C/C + + 的函数调用机制(我猜,很好地引入后续问题,写一个 sc_and做短路,这将涉及使用一个 #define而不是一个函数)。

你是否把三元操作符本身所做的称为短路(或者其他东西,比如“条件评估”) ,取决于你对短路的定义,你可以阅读各种各样的评论,以获得对短路的想法。对我来说是的,但是这和实际的问题或者为什么我称之为“把戏”并没有太大的关系。

当声明

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还是 false,在函数调用期间 b++总是被评估为 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函数参数的求值顺序未指定。

正如其他人已经指出的那样,无论什么作为两个参数传递到函数中,它都会在传递过程中进行计算。那是在租赁行动之前的事了。

另一方面,这个

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

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

C 三元运算符永远不能短路,因为它只计算一个表达式 (条件) ,以确定表达式 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}

表达式 可以由其他运算符形成,比如 & & 或 | | ,这些运算符可以短路,因为它们可以在返回一个值之前计算两个表达式,但是这不会被认为是短路的三元运算符,而是在条件中使用的运算符,就像在正则 if 语句中一样。

更新:

关于三元运算符是否为短路运算符存在一些争议。这个参数说任何不计算所有操作数的操作符都会根据下面注释中的@aruisdante 进行短路。如果给定这个定义,那么三元操作符将是短路,在这种情况下,这是原来的定义,我同意。问题是,术语“短路”最初是用于一种特定类型的操作符,允许这种行为,这些是逻辑/布尔操作符,为什么只有这些是我将试图解释的原因。

短路求值文章之后,这个短路求值只是指在语言中实现的布尔运算符,在这种情况下,知道第一个操作数会使第二个操作数变得不相关,这就是说,对于 & & 运算符是第一个操作数 假的,而对于 | | 运算符是第一个操作数 没错C11规格在6.5.13逻辑 AND 运算符和6.5.14逻辑 OR 运算符中也注明了这一点。

这意味着,对于要识别的短路行为,您希望在一个运算符中识别它,该运算符必须像布尔运算符那样计算所有操作数,如果第一个操作数不使第二个操作数变得不相关的话。这符合 数学工作室中“逻辑短路”部分中关于短路的另一个定义,因为短路来自逻辑运算符。

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

像往常一样,如果你发现有什么不对劲,请写一个评论,反对这一点,而不仅仅是反对票,这只会让 SO 的经验更糟糕,我相信我们可以成为一个更好的社区,一个只是反对票的答案,一个不同意。

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

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:

因此,正如其他人正确地指出的问题技巧是使你关注的三元操作员的行为,做短路(称为’条件求值’) ,而不是参数求值呼叫(按值呼叫) ,没有短路。

编辑以更正@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_and定义为 inline函数,您也将获得 没有 SCE。如果您像 @ alk那样将其定义为 C 宏,那么您肯定会这样做。