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.
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 10
    Yes for the built-in `,` operator. No if the `,` operator is user defined. see: http://en.cppreference.com/w/cpp/language/operator_other#Built-in_comma_operator (this changes with C++17) – Richard Critten Aug 22 '17 at 10:36
  • 19
    If you change the comma to a semicolon, you are guaranteed. :-) – Bo Persson Aug 22 '17 at 10:37
  • 8
    @BoPersson: Also, if I prepend the expression with `abort();` but that's not very helpful, is it? – einpoklum Aug 22 '17 at 10:39
  • @RichardCritten: So, in C++17, a user-defined `operator,` still has a sequencing guarantee? – einpoklum Aug 22 '17 at 10:44
  • 3
    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 happen – M.M Aug 22 '17 at 11:07
  • Possible duplicate of [Why are these constructs (using ++) undefined behavior?](https://stackoverflow.com/questions/949433/why-are-these-constructs-using-undefined-behavior) – Krupip Aug 22 '17 at 13:36
  • 1
    @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?) – einpoklum Aug 22 '17 at 13:38
  • @einpoklum same answer given. – Krupip Aug 22 '17 at 13:40
  • @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. – einpoklum Aug 22 '17 at 13:42
  • @einpoklum yet another duplicate https://stackoverflow.com/questions/24194076/in-c11-does-i-i-1-exhibit-undefined-behavior?rq=1 – Krupip Aug 22 '17 at 13:44
  • 1
    @snb: With due respect - it's another non-duplicate; I was asking about the behavior of expressions with a comma. – einpoklum Aug 22 '17 at 13:52
  • "_has not been overridden_" you mean **overloaded** – curiousguy Aug 22 '17 at 18:18
  • @curiousguy: Just edit the question instead of commenting - faster... – einpoklum Aug 22 '17 at 18:45

3 Answers3

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.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 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 code – M.M Aug 22 '17 at 10:55
  • @M.M - 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. – StoryTeller - Unslander Monica Aug 22 '17 at 10:59
  • 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](https://wandbox.org/permlink/vBFT8JpjfQThKw0E) 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... – user541686 Aug 22 '17 at 12:43
  • 1
    @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. – StoryTeller - Unslander Monica Aug 22 '17 at 12:45
  • 1
    @Mehrdad - And you can't really question the accuracy of the C++ standard itself on these matters. – StoryTeller - Unslander Monica Aug 22 '17 at 12:48
  • @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. – user541686 Aug 22 '17 at 12:51
  • @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.](https://timsong-cpp.github.io/cppwp/n4659/class.temporary#4). There is no context missing that warrants more of the standard IMO. – StoryTeller - Unslander Monica Aug 22 '17 at 12:53
  • @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. – user541686 Aug 22 '17 at 12:54
  • @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. – StoryTeller - Unslander Monica Aug 22 '17 at 12:55
  • 2
    @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... – user541686 Aug 22 '17 at 13:00
  • 1
    @Mehrdad - Yeah sure. Added another footnote – StoryTeller - Unslander Monica Aug 22 '17 at 13:08
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.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • 12
    And, to underscore, this is not unique to C++11. It has been the case since time immemorial. – Pete Becker Aug 22 '17 at 13:46
  • 1
    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. – Yttrill Aug 23 '17 at 01:35
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++));
Community
  • 1
  • 1
Deduplicator
  • 44,692
  • 7
  • 66
  • 118