在VC++和gcc中,std::exchange的工作方式不同

3

以下代码:

#include <utility>

int main()
{
  auto pos = 0;
  auto rel = pos - std::exchange(pos, pos + 1);

  return rel; // g++: 0, VC++: 1
}

如果您在VC++编译器上尝试使用rextester的代码,则结果为1;如果在godbolt上使用gcc,则结果为0(显然,在rextester中不使用gcc返回结果)。
问题:为什么结果会不同?
第二个问题:是否有工具可以检查这种错误?有任何clang警告吗?
我猜VC++中的std::exchange在评估其他操作数之前被调用,而在gcc中并非如此。 如果交换操作数posstd::exchange,则在VC++和gcc中都将得到-1(或255)的结果。
这可能与副作用相关,并且明确具有副作用的std::exchange函数被调用。
幸运的是,我通过单元测试捕获了从VC++转换到gcc后的错误,起初有些慌乱,但最终将其简化为此简单的(不起作用的)示例。

6
听起来像是未定义行为。没有要求它们必须从左到右进行评估。 - tadman
@tadman,你的评论有什么帮助吗? - michael_s
2个回答

4
二元运算符-没有关联到一个序列点,意味着在表达式A-B中不指定表达式AB的执行顺序。
考虑两个函数f()g()。在C和C++中,运算符+没有与序列点相关联,因此在表达式f()+g()中,可能会先执行f()或者g()。在C和C++中,对这样的表达式求值会产生未定义行为。
因此,你的程序有未定义的行为,任何跨编译器行为的分析都是徒劳的。
在标准规范中,这被称为[intro.execution]/17 [强调是我的]。

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [ Note: In an expression that is evaluated more than once during the execution of a program, unsequenced and indeterminately sequenced evaluations of its subexpressions need not be performed consistently in different evaluations.  — end note ] The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a memory location is unsequenced relative to either another side effect on the same memory location or a value computation using the value of any object in the same memory location, and they are not potentially concurrent, the behavior is undefined. [ Note: The next section imposes similar, but more complex restrictions on potentially concurrent computations.  — end note ]

[ Example:

void g(int i) {
  i = 7, i++, i++;  // i becomes 9

  i = i++ + 1;      // the value of i is incremented
  i = i++ + i;      // the behavior is undefined
  i = i + 1;        // the value of i is incremented
}

 — end example ]


我明白了 - 所以:基本上永远不要在带有二元(或任何?)操作符的表达式中使用具有副作用的代码。拥有这样的编译警告会很好。 - michael_s
阅读完intro.execution/18后,我相信这是未指定的行为,而不是未定义的。 - j6t
@j6t 我对[intro.execution]/18的理解是,在exchange函数内部进行的修改在计算值和与调用exchange中使用的任何参数表达式相关的副作用之后被排序。将此排序表示为“{传递给exchange的参数表达式(pos,pos + 1)>>序列下一个>> exchange中的修改}”。整个表达式,称为“{B}”,与操作符调用的另一个操作数,称为“{value compute pos}”,无序,因此“{value compute pos} - {B}”是无序的,并且[intro.execution]/17适用,从而存在UB。 - dfrib
但是intro.execution/18说:对于每个函数调用F,对于在F内发生的每个评估A和不在F内发生的每个评估B[...],要么A在B之前排序,要么B在A之前排序。 因此,在exchange子表达式外部的pos的评估与exchange的主体的排序是不确定的,因此我们有未指定的行为,而不是未定义的行为。 - j6t
案例a和b非常不同,因为f++ + f中的所有操作都是函数调用,因此参数的顺序是不确定的(自C++17以来)。我们有未指定的行为(自C++17以来),而不是未定义的行为。 - j6t
显示剩余8条评论

2
"未定义行为"是一项非常重要的原则,需要深入理解。如果你还没有学习过它,那么现在就有机会了。如果行为没有被明确定义,那么它就是未定义的,这意味着奇怪的事情可能会发生。

对于给定的表达式a - f(a),编译器执行的顺序完全没有规定。定义的行为仅指表示结果独立地评估af(a)

如果您希望程序产生一致、正确的结果,则避免未定义行为是其中之一的挑战。如何做到这一点呢?编译器不会告诉您何时发生未定义行为,它会在默默地发生,并且在您使用代码的所有条件下甚至可能“工作”。

如果您希望它真正正确,您需要知道许多像这样的规则,其中行为是未定义的,您只需要尊重它。这确实是无法避免的。就像处理迭代器失效、使用后释放以及使用未初始化变量时一样,您有很大的责任正确编写代码。这就是为什么C++编码可以异常具有挑战性的原因。

总之,其他语言对于类似a - f(a)这样的表达式执行顺序作出了明确的保证,而C++没有。


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