短路逻辑运算符是强制性的吗?还有求值顺序呢?

173

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

我很困惑,因为我记得K&R书中说你的代码不应该依赖于这些操作被短路,因为它们可能不会被短路。请问一下标准中是否指出了逻辑运算符总是被短路? 我主要关心C++,但也希望有C的答案。

我还记得阅读过(不记得在哪里),表达式中函数的执行顺序没有严格定义,所以你的代码不应该依赖或假设表达式中的函数会按特定顺序执行:在语句结束时,所有引用的函数都将被调用,但编译器可以自由选择最有效的顺序。

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

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

19
注意:这对于 POD 类型是正确的。但是,如果你为特定类重载了运算符 && 或运算符 ||,它们就不是快捷方式,我再次重申,不是快捷方式。这就是为什么建议你不要为自己的类定义这些运算符的原因。 - Martin York
我一段时间前重新定义了这些运算符,当时我创建了一个类来执行一些基本的布尔代数运算。也许应该加上一个警告注释“这会破坏短路和左右求值!”以防我忘记了这一点。此外,我还重载了*/+并将它们作为它们的同义词 :-) - Joe Pineda
在 if 块中进行函数调用并不是好的编程实践。请始终声明一个变量来保存方法的返回值,并在 if 块中使用它。 - S R Chaitanya
11
@SRChaitanya那不正确。你随意描述的不良实践经常发生,尤其是在返回布尔值的函数中,就像这里一样。 - user207421
对于那些需要帮助理解奇怪且不直观的术语“短路”的人,可以查看这篇文章:“短路”运算符的含义是什么? - undefined
7个回答

183

是的,在C和C++标准中,对于运算符||&&,短路和求值顺序是必需的。

C++标准表示(C标准应该有相应的条款):

1.9.18

在以下表达式的求值中

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

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

C++中有一个额外的陷阱:对于重载了||&&运算符的类型,短路不适用。

脚注12:本段指定的运算符是内置运算符,如第5条所述。当其中一个运算符在有效上下文中重载(参见第13条),从而指定用户定义的运算符函数时,表达式指定函数调用,操作数形成参数列表,它们之间没有隐含的序列点。

通常情况下,不建议在C++中重载这些运算符,除非你有非常特定的要求。虽然你可以这样做,但它可能会破坏其他人代码中期望的行为,特别是如果这些运算符通过实例化具有重载这些运算符的类型的模板间接地使用。


8
不知道短路规则不适用于过载逻辑运算,这很有趣。你能否请添加一个标准或来源的参考资料?我不是不信任你,只是想更多地了解这个问题。 - Joe Pineda
6
没问题,那很合理。它作为 operator&&(a, b) 的参数进行操作,具体的实现决定了其行为。 - Johannes Schaub - litb
12
litb:在不对 b 进行求值的情况下,无法将 b 传递给 operator&&(a,b)。而且,没有办法撤销对 b 的求值,因为编译器无法保证不存在副作用。 - jmucchiello
4
@Joe:但是运算符的返回值和参数可能从布尔类型变为其他类型。我曾经使用过三个值(“true”,“false”和“unknown”)来实现“特殊”的逻辑。返回值是确定性的,但是短路行为不合适。 - Alex B
3
标准中引用的部分保证了从左到右的求值顺序,但不保证短路。该保证可以在相关运算符的规范中找到。 - T.C.
显示剩余6条评论

79

短路求值和求值顺序是C和C++中强制执行的语义标准。

如果没有这个标准,像这样的代码将不会成为常见的习惯用法。

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

   }

C99规范的6.5.13逻辑与运算符部分(PDF链接)表示:

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

类似地,6.5.14逻辑或运算符部分表示:

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

在C++标准中可以找到类似的措辞,请查看此草案副本中的第5.14节。正如检查员在另一个答案中所指出的那样,如果您覆盖&&或||,则两个操作数都必须被评估,因为它变成了常规函数调用。


啊,这正是我在寻找的!好的,根据 ANSI-C 99 的规定,评估顺序和短路计算都是强制性的!不过,我真的很想看看 ANSI-C++ 的等效参考资料,虽然我几乎有99%的把握它应该是一样的。 - Joe Pineda
很难找到一个好的免费C++标准链接,我在谷歌上找到了一份草案副本并提供了链接。 - Paul Dixon
POD 类型为 True。但如果您重载运算符&&或||,这些就不是快捷方式。 - Martin York
1
是的,有趣的是对于布尔类型,您始终会拥有保证的评估顺序和短路行为。因为您无法为两种内置类型重载operator&&。您需要至少在操作数中有一个用户定义的类型才能使其行为不同。 - Johannes Schaub - litb
我希望我能够接受Checkers和这个答案。由于我对C++更感兴趣,所以我接受了另一个答案,但必须承认这个也非常出色!非常感谢你! - Joe Pineda

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 中: || 运算符从左到右分组。这两个操作数都将隐式转换为 bool 类型(第 4 条款)。如果其任一操作数为 true,则返回 true,否则返回 false。与 | 不同,|| 保证从左到右进行求值;此外,如果第一个操作数的求值结果为 true,则不会评估第二个操作数。
对于这两者接下来都有所描述:
结果是布尔值。第一个表达式的所有副作用(除了临时对象的销毁)发生在第二个表达式被计算之前。
此外,1.9/18 表示:
在每个表达式的求值中,使用这些表达式中运算符的内置意义(5.14、5.15、5.16、5.18),在第一个表达式的求值后存在一个序列点。

9

直接引用K&R的经典著作:

C语言保证逻辑运算符 &&|| 从左到右依次计算——我们很快就会看到这一点很重要。


3
"Expressions connected by && or || are evaluated left to right, and evaluation stops as soon as the truth or falsehood of the result is known. Most C programs rely on these properties." 这段话在《C 程序设计语言》第二版的第40页有提到。我并未引用过时的第一版。请问您需要了解什么其他信息吗? - Lundin
3
好的,看来你引用了这个古老的教程。它是1974年的,已经高度不相关了。 - Lundin

5

非常非常小心。

对于基本类型,这些是快捷操作符。

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

对于基本类型的 operator &&operator ||,评估顺序是从左到右的(否则缩短会很困难 :-) 但对于您定义的重载运算符,这些基本上是语法糖来定义方法,因此参数的评估顺序是未定义的。


2
运算符重载与类型是否为POD无关。定义运算符函数时,至少需要一个参数是类(或结构体或联合体)或枚举,或是其中之一的引用。POD意味着可以在其上使用memcpy。 - Derek Ledbetter
这就是我所说的。如果你为你的类重载&&,那么它实际上只是一个方法调用。因此,你不能依赖于参数的评估顺序。显然,你不能为POD类型重载&&。 - Martin York
4
你错误地使用了“POD类型”这个术语。无论是POD还是非POD类型的结构体、类、联合体或枚举,都可以重载"&&"运算符。但如果两边都是数值类型或指针,则不能重载"&&"运算符。 - Derek Ledbetter
我使用的是 POD 作为 (char/int/float 等),而不是聚合 POD(你所说的那个),通常被单独引用或更明确地引用,因为它不是内置类型。 - Martin York
3
所以你的意思是“基本类型”,但是却写了“POD类型”?(注:POD即Plain Old Data,指的是C++中一些特定的、与内存相关的数据类型) - Öö Tiib
@ÖöTiib:请看你评论上面的注释。 - Martin York

0

你的问题涉及到C++运算符优先级和结合性。基本上,在具有多个运算符且没有括号的表达式中,编译器通过遵循以下规则构造表达式树。

对于优先级,当你有像A op1 B op2 C这样的东西时,你可以将它们分组为(A op1 B) op2 CA op1 (B op2 C)。如果op1的优先级高于op2,你将得到第一个表达式。否则,你将得到第二个表达式。

对于结合性,当你有像A op B op C这样的东西时,你可以再次将其分组为(A op B) op CA op (B op C)。如果op具有左结合性,我们最终得到第一个表达式。如果它具有右结合性,我们最终得到第二个表达式。这也适用于相同优先级的运算符。

在这种情况下,&&的优先级高于||,因此表达式将被评估为(a != "" && it == seqMap.end()) || isEven
顺序本身是基于表达式树形式的“从左到右”。因此,我们首先评估a != "" && it == seqMap.end()。如果它为真,则整个表达式为真,否则我们转到isEven。当然,在左子表达式内部递归地重复该过程。

有趣的小知识,但优先级的概念源于数学符号。在 a*b + c 中也是如此,* 的优先级高于 +

更有趣/晦涩的是,对于一个未加括号的表达式 A1 op1 A2 op2 ... opn-1 An,其中所有运算符具有相同的优先级,我们可以形成的二进制表达式树的数量由所谓的卡特兰数给出。对于大的 n,这些数字增长得非常快。


所有这些都是正确的,但它涉及到运算符优先级和结合性,而不是求值顺序和短路。这些是不同的事情。 - Thomas Padron-McCarthy

0
如果你相信维基百科:

[&&||] 在语义上与位运算符 & 和 | 不同,因为如果可以仅通过左操作数确定结果,则它们永远不会评估右操作数

C (编程语言)


11
为什么我们有标准时还要信任维基百科! - Martin York
1
如果你相信维基百科,'维基百科不是一个可靠的资源' - user207421
这个说法在一定程度上是正确的,但不完整,因为C++中的重载运算符不支持短路。 - Thomas Padron-McCarthy

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