C++中的逻辑与+赋值运算符是否安全?

10

我刚学会了一个很棒的模式(实际上是从JavaScript学到的),我想将它应用到我的C++代码中。

为了解释这个模式,假设我将一个字符串表示为这些元素的链表:

struct link_char;
struct link_char
{
   link_char * next;
   char code;
};

请注意,任何link_char字符串的最后一个字符都将具有code==0。

这个属性意味着我可以在字符串中检查值,同时使用&&短路来防止空指针访问。

bool equals_hello( const link_char * first_char )
{
    const link_char * c = first_char;

    return       c->code=='h' 
    && (c=c->next)->code=='e' 
    && (c=c->next)->code=='l' 
    && (c=c->next)->code=='l' // if string == "hel", we short-circuit here
    && (c=c->next)->code=='o';
}

我的问题涉及安全性,而非可读性。 我知道当且仅当 && 没有被重载时,短路运算将起作用。但是,赋值操作是否会按正确顺序执行或者这是实现定义的呢?

上述示例明确说明了可以发生读取/写入的位置,但我也想在可能存在副作用的情况下使用此模式。例如:

// think of these as a bunch of HRESULT type functions 
//   a return value of 0 means SUCCESS
//   a return value of non-zero yields an Error Message
int err;
( !(err=initialize()) && !(err=create_window()) && !(err=run_app() )
    || handle_error(err);

这些操作跨平台是否按预期工作?我已经读过“如果你在一个表达式中两次读取一个变量,同时你也写了它,结果是未定义的”。但直观地感觉短路保证了顺序,难道不是吗?


3
我认为没问题是因为 && 是一个序列点,但我可能错了。 - GWW
1
关于您对可读性的评论;除非我想要在每个实例中都加上指向这个问题的链接,否则我不会提交这样的代码 :) - Merlyn Morgan-Graham
3
你的第一个示例在对 code 进行解引用之前不会检查是否为 NULL - Merlyn Morgan-Graham
1
在我看来,我不建议这种编码风格。所以我不关心它是否能够工作。 - Donotalo
1
链接条件和捕获错误代码并不是不合理的,但更容易阅读的方式是 if ((err = initialize()) || (err = create_window()) || (err == run_app())) handle_error(err); - Tony Delroy
显示剩余2条评论
2个回答

16
是的。 内置逻辑 AND(&&)、逻辑 OR(||)和逗号运算符(,)是 C++ 仅有的几种保证求值顺序的二元运算符。C++ 保证左表达式会被计算,然后(如果没有短路)再计算右表达式。(当然,逗号运算符总是计算两个操作数,首先是左操作数,然后是右操作数。)
还要注意的是,函数参数之间的逗号不是逗号运算符,因此函数参数的求值顺序未指定。更糟糕的是,在 f(g(h()),i()) 中,调用顺序可能是 h、i、g、f。
此外,求值顺序的保证仅适用于内置运算符;如果您重新定义它们,则它们基本上变成函数调用,在这种情况下,无法保证参数的求值顺序,也不执行短路。
其他二元运算符不保证求值顺序。例如,人们常犯的一个错误是认为在以下表达式中:
std::cout << foo() << bar();

调用foo()的操作保证发生在调用bar()之前...这是不正确的

(C++17修复了这个问题,但只在一些非常特殊的情况下才能实现,包括左移运算符,因为它被用于流)

当然,三目运算符:?的求值顺序也是有保证的,先求解条件表达式,再选择其中一个分支求值。

另一个求值顺序有保证的地方(对于新手来说有时会感到惊奇)是构造函数的成员初始化列表,但在这种情况下,顺序并不是表达式中的顺序,而是类声明中成员的顺序...例如:

struct Foo
{
   int x, y;
   Foo() : y(compute_y()), x(compute_x()) {}
};

在这种情况下,保证在成员声明中 x 出现在 y 之前,所以调用 compute_x() 的时间点一定早于调用 compute_y()


1
对于流操作符的观点加1分。确实很反直觉。 - cmeub
我认为在运算符重载的情况下并不保证(我的意思是在重载运算符&&等情况下)。这就是为什么建议不要重载这些运算符。千万别这样做。 - Tobias Langner
@Tobias:这已经在问题中说明了,但我仍然添加了澄清。谢谢。 - 6502
我只是想为阅读您精彩文章的读者添加这个建议。 - Tobias Langner

3
这些操作是否可以跨平台正常工作?我读到过“如果在表达式中两次读取变量并且在其中也写入它,则结果是未定义的”。但是直觉告诉我短路保证了顺序,不是吗?
内置的&&运算符具有保证的短路求值,这意味着它引入了一个序列点:C++98 §5.14/2“第一个表达式的所有副作用(除了临时对象的销毁(12.2))在第二个表达式被评估之前发生”。
所以对于C++来说没有问题。
尽管如此,我认为您建议的用法非常不好,因为它很难理解。不要使用您需要询问的语言功能,因为其他人可能同样不清楚它们。另外,在代码中加注释时,请注意Windows HRESULT在位31为1时表示失败,这与零/非零非常不同。
祝好!

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