【问题标题】:std::exchange working differently with VC++ and gccstd::exchange 与 VC++ 和 gcc 的工作方式不同
【发布时间】:2021-03-04 13:48:31
【问题描述】:

以下代码:

#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 显然不会返回结果)。

问题:为什么结果不同?

还有第二个问题:有没有工具可以检查这个错误?任何叮当警告?

我的猜测是,VC++ 中的std::exchange 在计算其他操作数之前被调用,而在 gcc 中却不是这样。如果您交换操作数 posstd::exchange,VC++ 和 gcc 的结果都是 -1(或 255)。

这可能与副作用有关 - 调用 std::exchange 显然有副作用。

幸运的是,在从 VC++ 转换到 gcc 后,我通过单元测试发现了这个错误 - 起初有点慌张,并将其归结为这个简单(不)工作的示例。

【问题讨论】:

  • 对我来说听起来像是未定义的行为。不要求从左到右评估它们。
  • @tadman 你的评论到底有没有帮助?

标签: c++ gcc visual-c++


【解决方案1】:

二元运算符-sequence point 相关联,这意味着未指定AB 将在A - B 中计算表达式的顺序:

考虑两个函数f()g()。在 C 和 C++ 中,+ 运算符与序列点无关,因此在表达式 f()+g() 中,f()g() 可能会首先执行。 [...] 在 C 和 C++ 中,评估这样的表达式会产生未定义的行为。[

因此,您的程序具有未定义的行为,对交叉编译器行为的任何分析都是徒劳的。


在标准语言中,这是[intro.execution]/17 [强调我的]:

除非另有说明,个人操作数的评估 运算符和单个表达式的子表达式是 未排序的。 [ 注意: 在计算的表达式超过 一次在程序执行期间,未排序和 其子表达式的不确定排序的评估不需要 在不同的评估中始终如一地执行。 — 尾注 ] 运算符的操作数的值计算是有序的 在算子结果的值计算之前。 如果一个 相对于,对内存位置的副作用是无序的 对同一内存位置或值的另一个副作用 使用同一内存中任何对象的值进行计算 位置,并且它们可能不是并发的,行为是 未定义。 [ 注意:下一节强加了类似的,但更多 对潜在并发计算的复杂限制。 — 结束 注意 ]

[ 示例:

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
}

 — 结束示例 ]

【讨论】:

  • 我明白了——所以:基本上永远不要在带有二进制(或任何?)运算符的表达式中使用带有副作用的代码。最好有编译警告。
  • @michael_s 编译器供应商通常专注于(考虑到语言的复杂性)按照标准要求实施诊断。不过,一个好的静态分析工具可能会警告您有关您自己的案例。
  • 我没有看到未定义的行为。函数的主体在表达式评估之前排序。子表达式的求值顺序是未指定的,但不是未定义的。
  • @j6t 虽然未指定评估子表达式的顺序,但如果我没记错的话,[intro.execution]/17 在这里适用,因为其中一个子表达式具有相同的副作用另一个子表达式读取的内存位置(“值计算”),并且 OP:s 示例未定义?
  • 是的,它适用。但是,修改仍然发生在exchange 内部,并且在函数调用的值计算之前排序。但现在我不确定这是否会提供任何额外的保证。请注意,[intro.execution]/17 所讨论的副作用是表达式中的运算符的副作用。调用函数内部发生的副作用不算在内。 OP 的示例仅在作业中具有副作用。
【解决方案2】:

"Undefined behaviour" 是一个非常重要的理解原则。如果您还没有了解它,那是您现在有机会解决的问题。如果行为没有具体定义,那么它就是未定义,这意味着像这样的奇怪事情可以而且将会发生。

对于给定的表达式a - f(a),绝对不需要编译器按从左到右的顺序执行。定义的行为仅说明结果表示af(a) 首先独立评估。

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

如果您希望它真正正确,您需要了解许多像这样的规则,其中行为是未定义的,您只需要尊重它。真的没有办法解决。就像处理迭代器失效、使用后释放和使用未初始化的变量一样,您有相当大的责任来正确编码。这就是为什么 C++ 很难正确编码的原因。

总之,其他语言在这样的表达式中对执行顺序做出了特定的保证。 C++ 没有。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-06
    相关资源
    最近更新 更多