是否必须使逻辑运算符短路?和评估顺序?


140

ANSI标准是否要求在C或C ++中使逻辑运算符短路?

我很困惑,因为我回想起K&R的书,说您的代码不应该依赖这些操作的短路,因为它们可能不是。有人可以指出标准中逻辑运算总是短路的地方吗?我对C ++最为感兴趣,对于C的回答也很好。

我还记得读(不记得在哪里)未严格定义评估顺序,因此您的代码不应依赖或假定表达式中的函数将按特定顺序执行:在语句末尾,所有引用的函数会被调用,但是编译器可以自由选择最有效的顺序。

标准是否指示此表达式的评估顺序?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";

12
细心:POD类型确实如此。但是,如果您重载了运算符&&或运算符||,对于特定班级,这些不是我重复的NOT快捷方式。这就是为什么建议您不要为自己的类定义这些运算符的原因。
马丁·约克

不久前,当我创建一个将执行一些基本布尔代数运算的类时,我重新定义了这些运算符。可能应该贴警告警告“这会破坏短路和左右评估!” 万一我忘记了这一点。还重载了* / +并使它们成为同义词:-)
Joe Pineda

在if块中进行函数调用不是一种好的编程习惯。始终声明一个变量,该变量保存方法的返回值,并在if块中使用它。
SR查塔尼亚

6
@SRChaitanya这是不正确的。您随意描述为不良做法的情况一直在发生,尤其是使用返回布尔值的函数时,如此处所示。
洛恩侯爵,

Answers:


154

是的,操作员||以及&&C和C ++标准都要求短路和评估顺序。

C ++标准说(C标准中应该有一个等效子句):

1.9.18

在评估以下表达式时

a && b
a || b
a ? b : c
a , b

使用这些表达式中运算符的内在含义,在第一个表达式(12)的求值之后有一个序列点

在C ++中有一个多余的陷阱:短路确实适用类型的超负荷运营商||&&

脚注12:本节中指出的运算符是内置运算符,如第5节所述。当这些运算符之一在有效上下文中被重载(第13节),从而指定了用户定义的运算符功能时,该表达式将指定函数调用,并且操作数形成一个参数列表,在它们之间没有隐含的序列点。

除非您有非常具体的要求,否则通常不建议在C ++中重载这些运算符。您可以执行此操作,但是这样做可能会破坏他人代码中的预期行为,尤其是如果通过实例化模板以使这些操作符类型超载的方式间接使用这些操作符时。


3
不知道短路是否不适用于过载的逻辑操作,这就是证明。您能否添加对标准的引用或来源?我并不是不信任您,只是想了解更多有关此的信息。
乔·皮内达

4
是的,这是合乎逻辑的。它充当operator &&(a,b)的参数。它的实现说明发生了什么。
Johannes Schaub-litb

10
litb:如果不进行评估,就不可能将b传递给operator &&(a,b)。而且无法撤消对b的求值,因为编译器不能保证没有副作用。
jmucchiello

2
我感到很难过。我本以为应该重新定义运算符&&和||。并且它们仍然是完全确定性的,编译器将检测到该问题并使其评估短路:毕竟,顺序是无关紧要的,并且它们保证没有副作用!
乔·皮内达

2
@Joe:但是运算符的返回值和参数可能会从布尔值更改为其他值。我曾经用三个值(“ true”,“ false”和“ unknown”)实现“特殊”逻辑。返回值是确定性的,但短路行为不合适。
Alex B

70

短路评估和评估顺序是C和C ++中强制性的语义标准。

如果不是这样的话,这样的代码将不是一个常见的习惯用法

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

6.5.13节C99规范的逻辑AND运算符(PDF链接)表示

(4)。与按位二进制&运算符不同,&&运算符保证从左到右的求值;在第一个操作数求值之后有一个序列点。如果第一个操作数比较等于0,则不评估第二个操作数。

同样,第6.5.14节“ 逻辑或”运算符表示

(4)与按位| 运算符,|| 运营商保证从左到右的评估;在第一个操作数求值之后有一个序列点。如果第一个操作数比较不等于0,则不计算第二个操作数。

在C ++标准中可以找到类似的措辞,请检查此草稿副本中的5.14节。正如检查人员在另一个答案中指出的那样,如果您覆盖&&或||,则必须对两个操作数进行求值,因为它会成为常规函数调用。


啊,我在找什么!好的,因此评估顺序短路均符合ANSI-C 99!我非常希望看到ANSI-C ++的等效参考,尽管我几乎99%都必须相同。
乔·皮内达

很难找到一个很好的C ++标准免费链接,并且已经链接到我通过谷歌搜索找到的草稿副本。
Paul Dixon

对于POD类型为True。但是,如果您重载了运算符&&或运算符||,这些不是捷径。
马丁·约克

1
是的,有趣的是,对于布尔来说,您始终可以保证评估顺序和短路行为。因为您不能重载两种内置类型的operator &&。您需要至少一个用户定义类型的操作数来使其表现不同。
Johannes Schaub-litb

我希望我可以接受Checkers和这个答案。由于我对C ++最为感兴趣,因此我接受另一种,尽管必须承认这也很棒!非常感谢你!
乔·皮内达

19

是的,它要求这样做(评估顺序和短路)。在您的示例中,如果所有函数都返回true,则调用顺序严格是从functionA开始,然后是functionB,然后是functionC。用于这样的

if(ptr && ptr->value) { 
    ...
}

逗号运算符相同:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

左和右操作数之间的一种表示&&||,和之间的第一和第二/第三个操作数?:(条件运算符)是一个“点序列”。在此之前,将完全评估任何副作用。因此,这是安全的:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

请注意,不要将逗号运算符与用于分隔事物的句法逗号混淆:

// order of calls to a and b is unspecified!
function(a(), b());

C ++标准在5.14/1

&&运算符组从左到右。操作数都隐式转换为bool类型(第4节)。如果两个操作数均为true,则结果为true,否则为false。与&不同,&&保证从左到右求值:如果第一个操作数为false,则不对第二个操作数求值。

5.15/1

|| 操作员组从左到右。操作数都隐式转换为布尔值(第4节)。如果两个操作数中的任何一个为true,则返回true,否则返回false。与|,||不同 保证从左到右的评估;此外,如果第一个操作数的值为true,则不计算第二个操作数的值。

它对旁边的两个都说:

结果是布尔。除临时变量(12.2)破坏外,第一个表达式的所有副作用都在第二个表达式求值之前发生。

除此之外,1.9/18说:

在评估每个表达式时

  • a && b
  • a || b
  • a ? b : C
  • a , b

使用这些表达式(5.14、5.15、5.16、5.18)中运算符的内在含义,在计算第一个表达式之后会有一个序列点。


10

直接来自良好的旧K&R:

C保证&&并且||从左到右进行评估-我们很快将看到在这方面很重要的情况。


3
K&R第二版p40。“由&&或||连接的表达式从左到右进行求值,并且一旦知道结果的真假就立即停止求值。大多数C程序都依赖于这些属性。” 我在书中的任何地方都找不到您引用的文字。这是从过时的第一版中摘录的吗?请说明您在何处找到此文字。
伦丁

1
好吧,事实证明您在引用这个古老的教程。它是从1974年开始的,与当时无关。
伦丁

6

要非常非常小心。

对于基本类型,这些是快捷方式运算符。

但是,如果您为自己的类或枚举类型定义这些运算符,则它们不是捷径。由于在不同情况下它们的用法在语义上存在差异,因此建议您不要定义这些运算符。

对于operator &&operator ||类型,评估顺序从左到右(否则,捷径将很难:-),但是对于您定义的重载运算符,它们基本上是定义方法的语法糖,因此参数的评估顺序为未定义。


1
运算符重载与POD类型无关。要定义运算符功能,至少一个自变量必须是类(或结构或联合)或枚举,或对其中之一的引用。成为POD意味着您可以在其上使用memcpy。
Derek Ledbetter,2009年

这就是我的意思。如果您为类重载&&,则实际上只是一个方法调用。因此,您不能依赖参数评估的顺序。显然,您不能为POD类型重载&&。
马丁·约克

3
您错误地使用了术语“ POD类型”。您可以为任何结构,类,联合或枚举(无论是否是POD)重载&&。如果双方都是数字类型或指针,则不能重载&&。
Derek Ledbetter,2009年

我将POD用作(char / int / float等)而不是Aggregate POD(这是您在说的),通常被单独或更明确地引用,因为它不是内置类型。
马丁·约克

2
所以您的意思是“基本类型”,但写了“ POD类型”吗?
二OO Tiib

0

您的问题归结于C ++运算符的优先级和关联性。基本上,在具有多个运算符且没有括号的表达式中,编译器通过遵循这些规则来构造表达式树。

作为优先事项,当您具有时A op1 B op2 C,可以将(A op1 B) op2 C或分组A op1 (B op2 C)。如果op1优先级高于op2,则将获得第一个表达式。否则,您将获得第二个。

为了具有关联性,当您有类似对象时A op B op C,可以再次将Thins分组为(A op B) op CA op (B op C)。如果op已离开关联性,我们将得到第一个表达式。如果它具有正确的关联性,我们将得出第二个结果。这也适用于相同优先级的操作员。

在这种情况下,&&优先级比更高||,因此该表达式将被评估为(a != "" && it == seqMap.end()) || isEven

顺序本身在表达式树形式上是“从左到右”的。因此,我们将首先进行评估a != "" && it == seqMap.end()。如果为true,则整个表达式为true,否则转到isEven。该过程将在课程的左子表达式内递归地重复。


有趣的花絮,但是优先权的概念源于数学符号。发生同样的事情a*b + c,其中的*优先级高于+

对于A1 op1 A2 op2 ... opn-1 An所有运算符具有相同优先级的无括号表达式,更有趣/晦涩的是,我们可以形成的二进制表达式树的数量由所谓的加泰罗尼亚数给出。总体而言n,这些增长非常快。d


所有这些都是正确的,但它与运算符的优先级和关联性有关,而与评估顺序和简短性无关。那是不同的东西。
Thomas Padron-McCarthy

0

如果您信任维基百科:

[ &&||]在语义上不同于按位运算符&和| |。因为如果只能从左边确定结果,他们将永远不会评估右边的操作数

C(编程语言)


11
当我们有了标准时,为什么还要信任维基!
马丁·约克

1
如果您信任维基百科,则“维基百科不是可靠的资源”
洛恩侯爵,

就目前而言,这是正确的,但不完整,因为C ++中的重载运算符不会短路。
Thomas Padron-McCarthy
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.