Question Details

No question body available.

Tags

c++ c++17 undefined-behavior sequence-points

Answers (3)

Accepted Answer Available
Accepted Answer
December 15, 2025 Score: 12 Rep: 228,654 Quality: Expert Completeness: 50%

Before C++17 that line is UB, so anything is possible, including calling f(2, 2);

Speculating on UB (undefined behavior) is mostly pointless. Compiler programmers aren't evil.

So for f(i++, i); we might say that we have f((i, sideeffect()), i);, and as sideeffect() cannot modify i without UB, we can move it before (or after):

i++; // can be placed before as it cannot modify i without UB
f(i, i);

and so we have f(2, 2).

December 15, 2025 Score: 17 Rep: 220,439 Quality: Expert Completeness: 50%

Summary:

Function arguments in C++ and C always had an unspecified order of evaluation. Meaning either left-to-right, or right-to-left, or middle first etc etc - whatever combination that is possible. It was designed this way to give compilers freedom, in order to encourage portability and performance.

Unspecified behavior means that when one of several different outcomes is possible, the compiler can do as it pleases and need not document which outcome it picks or how/when it does it. Nor does it need to pick the same outcome each time. But it shall not crash the program or produce other random outcomes, as might happen when you invoke undefined behavior (UB).

Before C++11 (and C11) there would be "sequence points", as in certain places where all expressions have to evaluated before progressing. One sequence point would be after the evaluation of the function arguments but before the function call.

Another rule said that if there were any side effects related to the same variable (like when writing to the same variable twice) between two sequence points, the code would invoke undefined behavior.

Then as per C++11 they changed this to sequenced before/sequenced after, but it is roughly the same rule still. The function argument evaluations are "unsequenced" in relation to each other - still UB.

C++17 didn't change any of this, but rather it made it so that one function argument evaluation must be sequenced before the next one. (Similarly they changed the assignment operator so that the right operator is sequenced before the left.) C and C++ are different here as per C++17.

Details:
https://en.cppreference.com/w/cpp/language/evalorder.html

December 15, 2025 Score: 7 Rep: 749 Quality: Medium Completeness: 100%

This is undefined behavior before C++17 due to a side effect on the memory location of i being unsequenced with a value computation using the value of i. See https://en.cppreference.com/w/cpp/language/evalorder.html for the detailed rules (and the changes since C++17).

C++17 fixes this by adding the following rule:

In a function call, value computations and side effects of the initialization of every parameter are indeterminately sequenced with respect to value computations and side effects of any other parameter.

By saying "indeterminately sequenced", it is no longer unsequenced (i.e. should not overlap) and therefore does not cause undefined behavior.

As @Jarod42 has pointed out, speculating the exact behavior of UB is mostly pointless. Here I'd like to show another more subtle and well-known case that is unspecified instead of undefined:

foo(std::uniqueptr(new Widget), bar())

Before C++17, the compiler is allowed to evaluate this expression in the following order:

  1. Perform new Widget
  2. Call bar()
  3. Call the constructor of std::uniqueptr

If bar() throws an exception, the new-ed object has not been passed into the uniqueptr, thus causing memory leak. That is why people recommended using std::makeunique instead of new + constructor before C++17. C++17 allows "1, 3, 2" or "2, 1, 3", but not "1, 2, 3".


As the comments pointed out, the original answer here made a mistake on explaining how could lead to f(2, 2). My focus is on showing the related and important example on the second part.