【问题标题】:Sequence Points and Method Chaining序列点和方法链
【发布时间】:2011-07-28 06:02:32
【问题描述】:

以下表达式通常用于演示 undefined 未指定的行为:

f() + g()

如果f()g() 都对某些共享对象有副作用,则行为是undefined 未指定,因为执行顺序未知。 f() 可以在 g() 之前评估,反之亦然。

现在我想知道当你在一个对象上链接成员函数时会发生什么。假设我有一个类的实例,该实例名为obj,它有两个成员函数foo()bar(),它们都修改了对象。这些函数的执行顺序不可交换。在另一个之前调用它们的效果与反过来调用它们的效果不同。这两种方法都返回对*this 的引用,因此它们可以像这样被链接起来:

obj.foo().bar()

但这是未指定的行为吗?我在标准中找不到任何东西(诚然只是浏览)可以区分这个表达式和我在帖子顶部给出的表达式。两个函数调用都是完整表达式的子表达式,因此它们的执行顺序是未指定的。但肯定foo() 必须首先被评估,以便bar() 知道要修改哪个对象。

也许我遗漏了一些明显的东西,但我看不到序列点的创建位置。

【问题讨论】:

  • 行为肯定是被定义的。但我无法指出标准的相关部分。
  • 正是我遇到的问题。我知道肯定必须定义它,但我就是看不到它。我只想关门!
  • 函数调用的结尾是一个序列点(1.9.17)。由于foo()的结果是bar()的参数(隐含的this-指针),所以顺序明确。
  • @Space_C0wb0y 太棒了!而已!我没有将 foo() 的结果实际上是 bar() 的参数这一事实联系起来。愿意将其发布为答案以便我接受吗?
  • @Space_C0wb0y 可以说隐含对象参数和隐含对象参数在重载决议之外不存在。这些构造仅存在于成员函数在重载决议期间与非成员函数可比较的情况下。第 13 节说“出于重载决议的目的,静态和非静态成员函数都具有隐式对象参数,但构造函数没有。”。这就是为什么我试图从规范的其他保证中推导出obj.foo().bar() 的行为。 (强调我的)。

标签: c++ method-chaining sequence-points


【解决方案1】:
f() + g()

这里的行为是unspecified(不是undefined),因为每个操作数的计算顺序(即每个函数被调用)是unspecified .

 obj.foo().bar();

这在 C++ 中是明确定义的。

来自 C++ ISO 标准的相关部分 §1.9.17 写道,

调用函数时(无论是 不是函数是内联的),有 评估后的序列点 所有函数参数的(如果有) 在执行之前发生 中的任何表达或陈述 函数体。还有一个 序列点复制后 返回值和 之前 执行外部的任何表达式 函数

类似案例已在以下主题中进行了详细讨论:

【讨论】:

  • 感谢您的回答。我将只添加一件事:调用 foo() 和调用 bar() 之间存在序列点的原因是因为 foo() 的结果作为隐式参数“this”传递。 (这是我遗漏的部分。感谢 Space_C0wb0y 指出)
  • @Nawaz a1.chain1().chain2() + a2.chain3().chain4() 怎么样?有没有机会像chain1 chain3 chain4 chain2一样被执行?
  • 特别是,这个 sn-p:stream.seek(4).read_int() - stream.seek(4).read_int() 应该返回 0,但在 MSVC 中返回垃圾,表明某些 .seek 被乱序调用。在这个 sn-p 之外,有问题的流类工作得很好,所以我正在寻找可能的 UB。
【解决方案2】:

如果 f() 和 g() 都对某些共享对象有副作用,则行为未定义,因为执行顺序未知。

这不是真的。函数调用不交错,在进入函数之前和离开函数之前都有一个序列点。 g 中的所有副作用与f 中的副作用至少由一个序列点分隔。行为不是未定义的。

因此,函数fg 的执行顺序是不确定的,但是一旦执行了一个函数,就只执行该函数的求值,而另一个函数“必须等待”。不同的可观察结果是可能的,但这并不意味着发生了未定义的行为。

现在我想知道当你在一个对象上链接成员函数时会发生什么。

如果您有obj.foo().bar(),那么您需要首先评估obj.foo() 以了解您调用函数bar 的对象,这意味着您必须等待obj.foo() 返回并产生一个值。然而,这并不一定意味着对obj.foo() 的评估引发的所有副作用都已结束。评估表达式后,您需要一个序列点,以使这些副作用被认为是完整的。因为在从obj.foo() 返回之前以及在调用bar() 之前有一个序列点,所以您有效地确定了执行副作用的顺序,这些副作用由分别评估foobar 中的表达式启动。

为了进一步解释,在您的示例中,foobar 之前被调用的原因与在下面的函数 f 被调用之前首先增加 i++ 的原因相同。

int i = 0;
void f() {
  std::cout << i << std::endl;
}

typedef void (*fptype)();
fptype fs[] = { f };

int main() {
  fs[i++]();
}

这里要问的问题是:这个程序会打印01,还是它的行为未定义或未指定?答案是,因为表达式fs[i++]必然要在函数调用之前先求值,而且在进入f之前有一个序列点,所以f里面i的值就是1

我认为你不需要将隐式对象参数的范围扩展到序列点来解释你的情况,你当然不能扩展它来解释这种情况(我希望这是定义的行为)。


C++0x 草案(不再有序列点)对此有更明确的措辞(强调我的)

调用函数时(无论函数是否内联),与任何参数表达式相关的每个值计算和副作用,或与指定被调用函数的后缀表达式相关联,在执行之前排序被调用函数体中的每个表达式或语句。

【讨论】:

  • 但是如果副作用是不可交换的呢? (那是你的上半场)
  • @Johannes:行为未指定,因为每个操作数的求值顺序未指定,对吧?
  • @Nawaz 是的,行为是未指定的,但每个可能的行为都有一个定义的结果。
  • 我在我的问题中更正了“未定义”->“未指定”。谢谢。
  • @Nawaz 谢谢。我认为对于fs[i++]() 示例很重要的是 1.9p7,它说“在称为序列点的执行序列中的某些指定点,先前评估的所有副作用都应该是完整的,并且后续评估的副作用不会发生” . 1.9p17 中描述的函数进入序列点导致在进入函数之前完成评估fs[i++] 产生的增量。当序列点和 1.9p7 消失后,规范不再涵盖这种情况,需要在 1.9p17 中添加明确的措辞。
【解决方案3】:

Foo() 将在 bar() 之前执行。我们必须先确定 Foo() ,否则我们将不知道 bar() 应该作用于什么(我们甚至不知道 bar() 属于什么类型。如果你认为它可能对你来说更直观,什么如果 foo 返回一个新的 obj 实例而不是 this,我们是否必须这样做?或者如果 foo 返回一个完全不同的类的实例,该类也定义了 bar() 方法。

您可以通过在 foo 和 bar 中使用断点并查看哪个首先被命中来自行测试。

【讨论】:

    猜你喜欢
    • 2023-03-05
    • 2014-03-03
    • 1970-01-01
    • 2011-11-06
    • 2016-05-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多