I was reading this question:
Undefined behavior and sequence points
and, specifically, the C++11 answer, and I understand the idea of "sequencing" of evaluations. But - is there sufficient sequencing when I write:
f(x++), g(x++);
?
That is, am I guaranteed that f()
gets the original value of x
and g()
gets a once-incremented x
?
Notes for nitpickers:
operator++()
has defined behavior (even if we've overriden it) and so do f()
and g()
, that no exceptions will be thrown, etc. - this question is not about that.operator,()
has not been overloaded.No, the behavior is defined. To quote C++11 (n3337) [expr.comma/1]:
A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value expression (Clause [expr]). Every value computation and side effect associated with the left expression is sequenced before every value computation and side effect associated with the right expression.
And I take "every" to mean "every"1. The evaluation of the second x++
cannot happen before the call sequence to f
is completed and f
returns.2
1 Destructor calls aren't associated with sub-expressions, only with full expressions. So you'll see those executed in reverse order to temporary object creation at the end of the full expression.
2 This paragraph only applies to the comma when used as an operator. When the comma has a special meaning (such when designating a function call argument sequence) this does not apply.
According to this evaluation order and sequencing reference the left hand side of the comma is fully evaluated before the right hand side (see rule 9):
9) Every value computation and side effect of the first (left) argument of the built-in comma operator , is sequenced before every value computation and side effect of the second (right) argument.
That means an expression like f(x++), g(x++)
is not undefined.
Note that this is only valid for the built-in comma operator.
First, let's assume that x++
by itself does not invoke undefined behavior. Think about signed overflow, incrementing a past-the-end-pointer, or the postfix-increment-operator might be user-defined).
Further, let's assume that invoking f()
and g()
with their arguments and destroying the temporaries does not invoke undefined behavior.
That are quite a lot of assumptions, but if they are broken the answer is trivial.
Now, if the comma is the built-in comma-operator, the comma in a braced-init-list, or the comma in a mem-initializer-list, the left and right side are sequenced either before or after each other (and you know which), so don't interfere, making the behavior well-defined.
struct X {
int f, g;
explicit X(int x) : f(x++), g(x++) {}
};
// Demonstrate that the order depends on member-order, not initializer-order:
struct Y {
int g, f;
explicit Y(int x) : f(x++), g(x++) {}
};
int y[] = { f(x++), g(x++) };
Otherwise, if x++
invokes a user-defined operator-overload for postfix-increment, you have indeterminate sequencing of the two instances of x++
and thus unspecified behavior.
std::list<int> list{1,2,3,4,5,6,7};
auto x = begin(list);
using T = decltype(x);
void h(T, T);
h(f(x++), g(x++));
struct X {
X(T, T) {}
}
X(f(x++), g(x++));
And in the final case, you get full-blown undefined behavior as the two postfix-increments of x
are unsequenced.
int x = 0;
void h(int, int);
h(f(x++), g(x++));
struct X {
X(int, int) {}
}
X(f(x++), g(x++));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With