With C++11, is it undefined behavior to write f(x++), g(x++)?
Asked Answered
I

3

42

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:

  • Assume that 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.
  • Assume that operator,() has not been overloaded.
Iata answered 22/8, 2017 at 10:34 Comment(13)
Yes for the built-in , operator. No if the , operator is user defined. see: en.cppreference.com/w/cpp/language/… (this changes with C++17)Irrigation
If you change the comma to a semicolon, you are guaranteed. :-)Concoction
@BoPersson: Also, if I prepend the expression with abort(); but that's not very helpful, is it?Iata
@RichardCritten: So, in C++17, a user-defined operator, still has a sequencing guarantee?Iata
In N4659, operator,( f(x++), g(x++) ); is not UB, and (say x == 0 beforehand) will either call f(0) then g(1), or call g(0) then f(1); and leave x == 2 afterwards. It was proposed that function calls have strict left-right evaluation, but so far as I know, that didn't happenStatuary
Possible duplicate of Why are these constructs (using ++) undefined behavior?Displace
@snb: Not at all, and please read the question and the potential dupe carefully before marking as dupe. (Hint: Which year was the other question asked on?)Iata
@Iata same answer given.Displace
@snb: The third or fourth answer regards C++11. While it's true that it answers this question, it doesn't quite answer that question, and again - it's pretty far down.Iata
@Iata yet another duplicate #24194576Displace
@snb: With due respect - it's another non-duplicate; I was asking about the behavior of expressions with a comma.Iata
"has not been overridden" you mean overloadedDemurrage
@curiousguy: Just edit the question instead of commenting - faster...Iata
R
49

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.

Roseleeroselia answered 22/8, 2017 at 10:37 Comment(11)
The first sentence of the quote could be misleading: it is only to be taken in the context of the comma operator. In the code func( f(x++), g(x++) );, we certainly have a pair of expressions separated by a comma, but this quote doesn't apply to that codeStatuary
@Statuary - That is covered by paragraph 2, a "special meaning" of the comma token. The OP did use the comma operator in a statement so I didn't think to quote it. Added a footnote.Roseleeroselia
Not sure if you're taking this too literally, if the compiler is wrong, or if I'm misunderstanding what "every value computation and side effect" includes, but if you look at this example you see that the left destructor's side effects only appear after right constructor is called, which makes me question the accuracy of this answer...Linkous
@Mehrdad - Object lifetime is an entirely different thing altogether. The object will not spring to life until it's evaluation in the comma operator comes, but it won't die until the entire expression is over. That's how temporaries are expected to behave.Roseleeroselia
@Mehrdad - And you can't really question the accuracy of the C++ standard itself on these matters.Roseleeroselia
@StoryTeller: I know it behaves that way and I know why it behaves that way, I'm just saying it seems to fly directly in the face of the quote here. The quote literally says "every value computation and side effect associated with the left expression", and you literally said you "take 'every' to mean 'every'", and surely the destructor call is a side effect "associated" with the left expression... so at best you need to add some context to your quote, or at worst either you or the standard is wrong/contradictory.Linkous
@Mehrdad - The constructor surely is part of the value computation, but the destructor is not. The side effects of the destructor have nothing to do with the value computation or side-effects of the value computation. And the objects have their lifetime specified as the full expression, explicitly.. There is no context missing that warrants more of the standard IMO.Roseleeroselia
@StoryTeller: You're just making an assertion here. Tell me what part of the quote actually supports what you're saying. It says "side effect associated with the left expression", not "side effect of the value computation", so you're not even reading it correctly.Linkous
@Mehrdad - I just gave you a link to a standard paragraph. The very last sentence of that paragraph is a good place for you to start.Roseleeroselia
@StoryTeller: Oh sorry, okay thanks, this is definitely counterintuitive: "The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression." Definitely not what I expected given that logically the destructor call for an expression is associated with it. I'd suggest adding to your answer that destructor calls are excluded from this, since it suggests otherwise to someone who hasn't read the standard...Linkous
@Mehrdad - Yeah sure. Added another footnoteRoseleeroselia
S
23

No, it isn't undefined behavior.

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.

Shocker answered 22/8, 2017 at 10:37 Comment(2)
And, to underscore, this is not unique to C++11. It has been the case since time immemorial.Chercherbourg
But the rules have changed sequence points are no longer used. I doubt the intent has changed, but the sequence point based explanation was internally incomplete and thus inconclusive.Gabrielegabriell
M
12

It depends.

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++));
Materse answered 22/8, 2017 at 13:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.