12

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 chain 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.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Joseph Mansfield
  • 108,238
  • 20
  • 242
  • 324
  • The behaviour is surely defined. But I couldn't point you to the relevant part of the standard. – Alexandre C. Apr 02 '11 at 12:51
  • Exactly the issue I'm having. I know that surely it must be defined but I just can't see it. I just want closure! – Joseph Mansfield Apr 02 '11 at 12:54
  • 6
    The end of a function call is a sequence point (1.9.17). Since the result of `foo()` is a parameter of `bar()` (the implicit `this`-pointer), the order is clearly defined. – Björn Pollex Apr 02 '11 at 12:56
  • @Space_C0wb0y Awesome! That's it! I wasn't connecting the fact that the result of foo() was actually a parameter of bar(). Care to post it as an answer so I can accept? – Joseph Mansfield Apr 02 '11 at 13:02
  • @Space_C0wb0y it can be argued that the implicit object parameter and the implied object argument don't exist outside overload resolution. These constructs only exist for member functions to be comparable against non-member functions during overload resolution. Section 13 says "*For the purposes of overload resolution*, both static and non-static member functions have an implicit object parameter, but constructors do not.". That's why I'm trying to derive the behavior of `obj.foo().bar()` from other guarantees of the spec. (emphasize mine). – Johannes Schaub - litb Apr 02 '11 at 13:26
  • Just to clarify for future readers: There is a *big* difference between behavior being "undefined" and behavior being "unspecified." Undefined means anything could happen, and the code is non-conformant. Unspecified means you can tell exactly what will happen, and the code is conformant, but you can't say *in what order* things will happen. – John Dibling Apr 02 '11 at 15:10

3 Answers3

10
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:

Community
  • 1
  • 1
Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • 1
    Thanks for this answer. I'll just add one thing: The reason there is a sequence point between the call to foo() and the call to bar() is because the result of foo() is passed as the implicit parameter "this". (This is the part that I was missing. Thanks to Space_C0wb0y for pointing it out) – Joseph Mansfield Apr 02 '11 at 13:28
  • @Nawaz what about `a1.chain1().chain2() + a2.chain3().chain4()`? Is there any chance it gets executed like `chain1 chain3 chain4 chain2`? – rr- Dec 20 '15 at 13:37
  • In particular, this snippet: `stream.seek(4).read_int() - stream.seek(4).read_int()` should return 0, but returns garbage in MSVC that indicates some `.seek` gets called out of order. Outside this snippet, the stream class in question works very well, so I'm looking for possible UBs. – rr- Dec 20 '15 at 13:43
5

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.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • But what if the side effects are non-commutative? (That was to your first half) – Joseph Mansfield Apr 02 '11 at 13:16
  • @Johannes: The behavior is unspecified, because the order in which each operand is evaluated is unspecified, right? – Nawaz Apr 02 '11 at 13:19
  • @Nawaz yes the behavior is unspecified, but each possible behavior has a defined result. – Johannes Schaub - litb Apr 02 '11 at 13:21
  • I made the correction 'undefined' -> 'unspecified' in my question. Thanks. – Joseph Mansfield Apr 02 '11 at 13:23
  • @Johannes: I liked your example of `fs[i++]` to explain §1.9.17 which it does so clearly now. +1 for it. – Nawaz Apr 02 '11 at 15:11
  • 1
    @Nawaz thanks. What I think is important for the `fs[i++]()` example is 1.9p7 which says "At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place". The function entry sequence point described in 1.9p17 causes the increment produced by the evaluation `fs[i++]` to be completed before the function is entered. When sequence points and 1.9p7 were gone, the spec did not cover this case anymore, and explicit wording needed to be added at 1.9p17. – Johannes Schaub - litb Apr 02 '11 at 15:41
1

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.

Kindread
  • 926
  • 4
  • 12