【问题标题】:GCC bug? Chaining methods, broken sequence point海湾合作委员会错误?链接方法,断序列点
【发布时间】:2014-03-03 21:20:04
【问题描述】:

我已经调试了一段时间的程序,最终发现错误是由于引用没有像我想象的那样更新。

这是一个显示我遇到的问题的示例:

#include <iostream>
using namespace std;

struct Test {
    Test& set(int& i){ i = 10; return *this; }
    Test& print(const int& i){ cout << i << endl; return *this; }
};

int main(void){
    int i = 0;
    Test t;

    t.set(i).print(i + 5);

    return 0;
}

我原以为这里的 print() 方法会输出 15,结果却输出了 5。

编辑:10 天后,我才意识到使用 clang 输出 15!这是 GCC 中的错误吗?

【问题讨论】:

  • 为什么要编写这样既难看又总是容易出错的代码?为什么需要链接方法?
  • @EdHeal 我有一个包含许多方法的类,每个方法都做的很少,并且需要连续调用很多方法。我选择允许这样的链接以节省相当多的打字时间。现在我发现了这种行为,我会考虑删除链接。
  • 速度太快了。
  • 您将0 + 5 传递给print(),然后直接打印。下次调试你的代码。
  • @StoryTeller 那为什么g++和clang++的输出不同呢?

标签: c++ gcc compiler-bug


【解决方案1】:

让我试着解释一下 C++11 标准。在 §1.9/15 中它说:

除非另有说明,单个运算符的操作数和单个表达式的子表达式的求值是无序的。 [...] 如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。

当然int 是一个标量类型,而t.set(i).print(i + 5);set() 和值计算i + 5 中包含对i 的副作用,所以如果没有另外说明,行为确实是未定义的。阅读第 5.2.5 节(“类成员访问”),我找不到关于 . 运算符的序列的任何注释。 [但请参阅下面的编辑!]

注意,当然可以保证set()print() 之前执行,因为后者接收前者的返回值作为(隐式this)参数。 这里的罪魁祸首是 print 的参数的值计算是 unsequenced 相对于 set 的调用是不确定的。

编辑:在阅读了您(@Xeno's)评论中的答案后,我重新阅读了标准中的段落,实际上它后来说:

调用函数中的每个求值(包括其他函数调用)在被调用函数的主体执行之前或之后没有特别排序的,相对于被调用函数的执行是不确定的。

因为 indeterminately sequenced 不是 unsequenced(“未排序评估的执行可以重叠”,第 1.9/13 节),这确实不是未定义的行为,而是“只是”未指定的行为,这意味着 15 和 5 都是正确的输出。

所以当&lt; 表示“之前排序”而~ 表示“不确定排序”时,我们有:

(value computations for print()'s arguments ~ execution of set()) &lt; execution of print()

【讨论】:

  • This SO answer 指的是 C++ 标准的早期版本,并声明函数调用的结束是一个序列点。 Cppreference 表示“如果子表达式 E1 和 E2 之间存在序列点,则 E1 的值计算和副作用都会在 E2 的每个值计算和副作用之前排序”。因此我不确定是否存在任何未定义的行为(至少根据以前的标准)。
  • @Xeno:再看看这个答案的最后一段。
  • 您的附加编辑解释似乎是正确的。也许标准应该包括一个类似于逗号运算符的链式方法的“sequenced before”规则,以使这种行为具有确定性,尽管我认为这可能会引入其他缺陷。
【解决方案2】:

[评论太长:]

如果添加

Test& add(int& i, const int toadd)
{
  i += toadd;
  return *this;
}

这个电话

t.set(i).add(i, 5).print(i);

返回

15

由此我得出结论,罪魁祸首是 i + 5 作为打印参数。

【讨论】:

    【解决方案3】:

    C++ 不能保证单个表达式中的函数参数的计算顺序,即使这些函数是链式方法调用也是如此。你在这里调用了未定义的行为,这就是你得到的。

    .运算符 确实 意味着排序,但仅限于 . 之前的表达式。在访问成员之前必须对其进行全面评估。这并不意味着子表达式的计算在此之前被暂停。

    另外,不要通过const int&amp; 传递ints,这不可能比直接传递int 更快(除非出于某种奇怪的原因int 不适合处理器字并且参考确实)。

    【讨论】:

    • 我的印象是点运算符“。”是一个序列点,之前和之后的所有内容都必须按顺序计算。
    • @Xeno 查找它。如果是这种情况,我很抱歉,我会删除这个答案。
    • @Xeno 我阅读标准的方式,唯一可以保证的是对 set 的调用在对 print 的调用之前排序,我实际上没有发现任何暗示对print 的所有参数的评估必须在调用set 之后进行排序。如果有人能指出标准的部分内容是这样的话,这确实是 gcc 中的一个错误。
    • Cppreference 声明“如果子表达式 E1 和 E2 之间存在序列点,则 E1 的值计算和副作用都会在 E2 的每个值计算和副作用之前排序”。那么E1中i=10的副作用应该在E2中i+5的值计算之前排序,不是吗?
    • @Xeno 它需要在 print 被调用之前发生,而不是在 i+5 被评估之前发生。
    猜你喜欢
    • 2014-11-06
    • 2016-06-07
    • 1970-01-01
    • 1970-01-01
    • 2021-07-15
    • 1970-01-01
    • 1970-01-01
    • 2016-09-23
    • 1970-01-01
    相关资源
    最近更新 更多