This answer says that
i = 1;f(i++, i)will not evaluate tof(2, 2) since C++17.
However, I'm struggling to understand why this would have been possible before C++17.
What operations, and in what order, could occur to arrive at2, 2?
How would the first argument off end up being passed the value2 since by definition,i++ always yields the old value and nothing incrementsi apart from this?
- 5Before C++17 that line is UB, so anything is possible, including calling
f(2, 2);cigien– cigien2025-12-15 11:49:28 +00:00Commentedyesterday - The linked answer explains that in the very first line - it wasunspecified before C++ 17, so every compiler could do whatever was easiest.Panagiotis Kanavos– Panagiotis Kanavos2025-12-15 11:49:58 +00:00Commentedyesterday
- 1@FluidMechanicsPotentialFlows Yes, exactly. Setting
ito 47 would be strange (and I doubt a compiler would actually do that), but it's absolutely allowed.cigien– cigien2025-12-15 11:53:14 +00:00Commentedyesterday - 5As, I think,famously observed in a not-dissimilar situation in Perl once "But in other cases, pink monkeys may fly out of your nose, your dog may die and your piano suddenly walks away. The behaviour of $i = $i ++ in all its variants is undefined. Anything could happen." I seem to remember hearing that cited back at university in the 1990s so I'm not sure of the exact original source, but it gives you the full flavour of what's actually meant by undefined behaviour.Matt Gibson– Matt Gibson2025-12-15 12:07:48 +00:00Commentedyesterday
- 2Even if this was all fine... why would you write such ambiguous and hard to debug code? The advice I will give you is limit the number of (side) effects to one per line. Your code will be more readable, easier to debug and in the end the compiler will not care and still produce the same (highly) optimized code.Pepijn Kramer– Pepijn Kramer2025-12-15 13:48:00 +00:00Commentedyesterday
4 Answers4
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 invokeundefined 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 beafter 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/eval_order.html
3 Comments
f(++i, ++i); // undefined behavior until C++17, unspecified after C++17, what exactly do they mean then? Is this related to when you say "one function argument evaluation must be sequenced before the next one"? meaning we can't have twice the same value passed to f (and we could before)?i=1;f(++i,++i) can only evaluate tof(2,3) orf(3,2). Prior to that, it would be undefined behavior: it would be permissible to producef(2,2) orf(3,3) -- or for the compiler to reason that, since a well-formed program cannot contain undefined behavior and the function call inevitably produces undefined behavior, the entire program flow leading to the function call must be dead code and can be optimized out.func(f(++i), f(++i)) and letf be a function-like macro, which unlike a function does not add sequencing.Before C++17 that line is UB, so anything is possible, including callingf(2, 2);
Speculating on UB (undefined behavior) is mostly pointless.Compiler programmers aren't evil.
So forf(i++, i); we might say that we havef((i, side_effect()), i);, and asside_effect() cannot modifyi without UB, we can move it before (or after):
i++; // can be placed before as it cannot modify i without UBf(i, i);and so we havef(2, 2).
14 Comments
i++ always yielding the old value?sideeffect(); andf(i, i); should not be sequenced, so either can happen first.f((i, side_effect()), i)) whereas I meantf(side_effect(), old_i), old_i) to return ("old")i, andold_i andi might be merged when there are no UBs.f((side_effect(i), result_of_side_effect), i) (whereresult_of_side_effect is actually neitheri nor a reference toi) may turn intof(i, i). Doesn't it change observable behavior, which the compiler must comply with?f(2, 2) with some logic.result_of_side_effect andi might be seen as the same.f(2, 2) were possible. Since C++17, sequenced in unspecified order, so onlyf(1, 2) orf(1, 1) are possible (and it might change for every call).f(2,2) anywayThis is undefined behavior before C++17 due to a side effect on the memory location ofi being unsequenced with a value computation using the value ofi. Seehttps://en.cppreference.com/w/cpp/language/eval_order.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 isunspecified instead ofundefined:
foo(std::unique_ptr<Widget>(new Widget), bar())Before C++17, the compiler is allowed to evaluate this expression in the following order:
- Perform
new Widget - Call
bar() - Call the constructor of
std::unique_ptr<Widget>
Ifbar() throws an exception, thenew-ed object has not been passed into theunique_ptr, thus causing memory leak. That is why people recommended usingstd::make_unique instead ofnew + 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 tof(2, 2). My focus is on showing the related and important example on the second part.
2 Comments
f(2,2), as this answer claims. As far as I can tell, it will rather lead tof(1, 1).In C and C++ calling conventions, arguments to a function are pushed to the stack right-to-left order. This means that the left-most argument is a the top of the stack (in stack-logical order; this may well be at the lowest physical address). This allows variable argument functions to see the first argument at the top of the stack. Which allows it to be accessed without having to take in account the number/size of arguments passed in.
Therefor,
f(i++, j++);can be implemented in assembly as
temp = j++push temp;temp = i++; push temp; jump-to-subroutine fpop;pop;In C and C++ the caller removes the arguments to the stack
If the functions is called as
i=1;f(i++, i);it should be clear whyf(2, 1); is the resultant call.
4 Comments
stdcall andfastcall. Anyway, at best this answer can roughly explain some observed behaviour from debug-mode builds (in an over-simplified way, leaving out the fact that arg eval order doesn't have to match push order in a calling convention with register args.)Explore related questions
See similar questions with these tags.





