序列点和方法链

时间:2011-04-02 12:48:47

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

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

f() + g()

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

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

obj.foo().bar()

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

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

3 个答案:

答案 0 :(得分:10)

f() + g()

此处行为未指定(不是未定义),因为评估每个操作数的顺序(即,调用每个函数)是未指定的

 obj.foo().bar();

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

C ++ ISO标准的相关章节§1.9.17读取,

  

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

在这些主题中已经详细讨论了类似的案例:

答案 1 :(得分:5)

  

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

事实并非如此。函数调用不进行交错,在进入函数之前和离开函数之前有一个序列点。 g中与f中的副作用相关的所有副作用由至少一个序列点分开。行为未定义。

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

  

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

如果您有obj.foo().bar(),则需要先评估obj.foo()以了解您调用函数bar的对象,这意味着您必须等待obj.foo()返回并产生一个价值。然而,这并不一定意味着评估obj.foo()引发的所有副作用都已完成。在评估表达式后,您需要序列点才能将这些副作用视为完整。因为在从obj.foo()返回之前以及在调用bar()之前存在序列点,所以您实际上已经确定了通过评估foobar中的表达式而启动的副作用的顺序分别。

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

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

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

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

这里要问的问题是:此程序是打印01还是其行为未定义或未指定?答案是,因为必须首先在函数调用之前评估表达式fs[i++],并且在输入f之前有一个序列点,if内的值}是1

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


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

  

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

答案 2 :(得分:1)

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

您可以使用foo和bar中的断点自行测试,并查看哪个断点首先被击中。