12

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

f() + g()

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

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

obj.foo().bar()

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

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

4

3 回答 3

10
f() + g()

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

 obj.foo().bar();

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

C++ ISO 标准中的相关章节 §1.9.17 内容如下:

调用函数时(无论函数是否内联), 在对所有函数参数(如果有的话) 求值之后都会有一个序列点,该序列点发生在函数体中的任何表达式或语句执行之前。在复制返回值之后和执行函数外的任何表达式之前还有一个 序列点

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

于 2011-04-02T13:13:27.587 回答
5

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

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

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

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

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

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

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

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

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

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

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


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

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

于 2011-04-02T13:14:48.583 回答
1

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

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

于 2011-04-02T13:06:21.157 回答