开发者

Sequence Points and Method Chaining

开发者 https://www.devze.com 2023-02-21 20:01 出处:网络
The following expression is often used to demonstrate undefined unspecified behaviour: f() + g() If f() and g() both have side effects on some shared object then the behaviour is undefined unspecif

The following expression is often used to demonstrate undefined unspecified behaviour:

f() + g()

If f() and g() both have side effects on some shared object then the behaviour is undefined unspecified because the order of execution is unknown. f() may be evaluated before g() or vice versa.

Now I was wondering what happens when you cha开发者_如何学Cin member functions on an object. Let's say I have an instance of a class, the instance called obj and it has two member functions, foo() and bar() which both modify the object. The order of execution of these functions is not commutative. The effect of calling one before the other is not the same effect as calling them the other way around. Both methods return a reference to *this so that they can be chained like so:

obj.foo().bar()

But is this unspecified behaviour? I can't find anything in the standard (admittedly just scanning through) that differentiates between this expression and the expression I gave at the top of the post. Both function calls are subexpressions of the full-expression and so their order of execution is unspecified. But surely foo() must be evaluated first so that bar() knows which object to modify.

Perhaps I'm missing something obvious, but I can't see where a sequence point is created.


f() + g()

Here the behavior is unspecified (not undefined), because the order in which each operand is evaluated (that is, each function is called) is unspecified.

 obj.foo().bar();

This is well-defined in C++.

The relevant section §1.9.17 from the C++ ISO standard reads,

When calling a function (whether or not the function is inline), there is a sequence point after the evaluation of all function arguments (if any) which takes place before execution of any expressions or statements in the function body. There is also a sequence point after the copying of a returned value and before the execution of any expressions outside the function.

Similar cases has been discussed in great detail, in these topics:

  • Undefined behavior and sequence points reloaded
  • Is this code well-defined?


If f() and g() both have side effects on some shared object then the behaviour is undefined because the order of execution is unknown.

This is not true. Function invocations do not interleave, and there is a sequence point before entering functions, and before leaving functions. All side effects in g respective to side effects in f are separated by at least one sequence point. Behavior is not undefined.

As a consequence, the order of execution of the functions f and g is not determined, but once one function is executed, only that function's evaluations are executed, and the other function "has to wait". Different observable results are possible, but this does not imply that undefined behavior has happened.

Now I was wondering what happens when you chain member functions on an object.

If you have obj.foo().bar() then you need to first evaluate obj.foo() to know what object you call function bar on, which means you have to wait for obj.foo() to return and yield a value. This however does not necessarily mean that all side effects initiated by evaluation of obj.foo() are finished. After evaluating an expression, you need a sequence point for those side effects to be considered complete. Because there is a sequence point before returning from obj.foo() and also before calling bar(), you have effectively a determined order for executing side effects initiated by evaluating expressions in foo and bar respectively.

To explain a bit more, the reason foo is called before bar in your example is the same to why i++ is first incremented before the function f is called in the following.

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

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

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

The question to ask here is: Will this program print 0, 1 or is its behavior undefined or unspecified? The answer is, because the expression fs[i++] necessarily has to be first evaluated before the function call, and there is a sequence point before entering f, the value of i inside f is 1.

I don't think you need to extend the scope of the implicit object parameter to sequence points to explain your case, and you certainly cannot extend it to explain this case (which I hope is defined behavior).


The C++0x draft (which doesn't have sequence points anymore) has a more explicit wording of this (emphasize mine)

When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.


Foo() will be executed before bar(). We have to determine Foo() first, or else we won't know what bar() should act on ( we wont even know what type of class bar() belongs to. It might be more intuitive to you if you think, what would we have to do if foo returned a new instance of obj, instead of this. Or if foo returned an instance of an entirely different class that also has a bar() method defined.

You can test this yourself by using breakpoints in foo and bar and seeing which gets hit first.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号