【问题标题】:Order of evaluation in v != std::exchange(v, predecessor(v))v != std::exchange(v, predecessor(v)) 中的求值顺序
【发布时间】:2022-11-29 00:50:10
【问题描述】:

我一直在寻找更多适用于 std::exchange 的习语。

今天我发现自己writing this 在一个答案中:

do {
    path.push_front(v);
} while (v != std::exchange(v, pmap[v]));

我比说更喜欢它

do {
    path.push_front(v);
    if (v == pmap[v])
        break;
    v= pmap[v];
} while (true);

希望出于显而易见的原因。

但是,我不太喜欢标准语,我不禁担心 lhs != rhs 不能保证右侧表达式在左侧表达式之前没有被完全计算。这将使它成为一个同义反复的比较——根据定义它会返回 true。

然而,代码确实运行正确,显然首先评估了 lhs。

有人知道吗

  • 标准是否保证这个评价顺序
  • 如果它在最近的标准中发生了变化,哪个标准版本首先规定了它?

附言。我意识到这是 f(a,b) 的特例,其中 foperator!=。我试图使用此处找到的信息回答我自己的查询,但迄今为止未能得出结论:

【问题讨论】:

  • 嗯...如果operator != 是成员timsong-cpp.github.io/cppwp/n4868/expr.call#8,则可能格式正确 - 它肯定不会比标准自己的示例复杂
  • 我没有看到任何要求 != 的左侧在右侧之前排序的措辞。 C++17 为某些操作添加了顺序,但 != 似乎不在其中。
  • @rturrado 我喜欢认为“原子”(如单语句)交换的循环更清晰。但是,是的,没有它似乎更安全。哪个恕我直言是不明显的部分。我让危险传感器关闭的唯一原因是因为我在过去吸取了痛苦的 C++ 教训。但我希望普通程序员对代码应该做什么有相同的、完全直观的期望。
  • @rturrado 是的,双 pmap[v] 可以通过添加命名变量来规避。 las,没有办法限制所述变量的范围。重写为 for 循环(通常)需要使推送操作成为条件的副作用(这在客观上更糟,因为与std::exchange 不同,该操作还不存在具有众所周知的语义)或者...在循环体之外复制它...这是第 22 条军规
  • FWIW

标签: c++ language-lawyer sequence-points


【解决方案1】:

C++17 在sequences 上引入了规则。以前的 UB 现在定义明确了。这适用于函数调用的参数以及一组精选的运算符:

sequenced before 是一种不对称的、可传递的、成对的关系 在同一线程内的评估之间。

  • 如果 A 在 B 之前排序(或者等效地,B 在 A 之后排序),则 A 的评估将在 B 的评估之前完成 开始。

然而内置的!=不是排序(见上面的链接)。将对函数调用进行排序,但无法保证评估的顺序:

  1. 在函数调用中,值计算和副作用 每个参数的初始化是不确定地顺序为 关于任何其他参数的值计算和副作用。

(强调)

根据我的阅读,即使你写了一个包装函数,你的编译器也不需要先评估v,然后评估std::exchange(v, pmap[v]),最后评估equal(..)。我相信,颠倒评估顺序会改变您示例中的语义。

可悲的是,尽管std::exchange 很好,但在这种情况下,不能保证它能满足您的需要。

【讨论】:

  • 编辑:对不起,我误读了一点。更正答案。
  • 哈。在你意识到并开始编辑之前,我已经将that quote复制到评论中。有点难过。好像是这样的样子¯_(ツ)_/¯
  • @sehe 是的,我的大脑以某种方式将“不确定地”一词解析为“确定地”。 :)
  • @bitmask 不同的调用约定对评估顺序有不同的偏好。历史上最流行的一种是基于堆栈的 cdecl,它更喜欢从右到左。
  • @bitmask 很久以前,Hyman Rosen写了一个paper来强制执行严格的从右到左的评估顺序。不幸的是,那篇论文无处可去。
【解决方案2】:

这是每个 [intro.execution]/10 的 UB:

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的求值是无序的。

[...] 如果一个内存位置的副作用相对于同一内存位置的另一个副作用或使用同一内存位置中任何对象的值的值计算是未排序的,并且它们可能不是并发的,则行为未定义。

!= 运算符 does not 具有任何特殊的排序属性。)

!=是否为v的类型重载不会影响排序规则(因为您没有使用函数调用符号调用它)([over.match.oper]/2)

[...] 运算符符号首先转换为等效的函数调用符号 [...] 但是,操作数按照为内置运算符规定的顺序排序。

(即使你确实使用了函数调用符号,操作数仍然是indeterminately sequenced。)


此外,即使 != 的左侧在右侧之前排序,您的第一个 sn-p 中的条件也始终是 false - 您混淆了操作数的顺序。我认为这充分说明了编写此代码的方式更清晰。

【讨论】:

    猜你喜欢
    • 2019-07-30
    • 1970-01-01
    • 1970-01-01
    • 2021-08-26
    • 2019-05-31
    • 1970-01-01
    • 2019-05-07
    • 2021-11-22
    • 2021-12-31
    相关资源
    最近更新 更多