8

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?

Peter Cordes's user avatar
Peter Cordes
378k50 gold badges745 silver badges1k bronze badges
askedyesterday
FluidMechanics Potential Flows's user avatar
17
  • 5
    Before C++17 that line is UB, so anything is possible, including callingf(2, 2);Commentedyesterday
  • The linked answer explains that in the very first line - it wasunspecified before C++ 17, so every compiler could do whatever was easiest.Commentedyesterday
  • 1
    @FluidMechanicsPotentialFlows Yes, exactly. Settingi to 47 would be strange (and I doubt a compiler would actually do that), but it's absolutely allowed.Commentedyesterday
  • 5
    As, 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.Commentedyesterday
  • 2
    Even 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.Commentedyesterday

4 Answers4

13

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

answeredyesterday
Lundin's user avatar
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the link. When they say: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)?
Under C++17,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.
@FluidMechanicsPotentialFlows Those are two side effects on the same variable and unsequenced in relation to each other, so UB. Whereas after C++17 you merely get unspecified behavior: the program won't behave strangely, but you don't get to know if the left ++i is executed before or after the right one. Which normally doesn't matter. You could play around with this by doing for examplefunc(f(++i), f(++i)) and letf be a function-like macro, which unlike a function does not add sequencing.
12

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).

answeredyesterday
Jarod42's user avatar

14 Comments

But isn't the "side_effect" supposed to happenafter passing the argument to f by definition ofi++ always yielding the old value?
To be UB free,sideeffect(); andf(i, i); should not be sequenced, so either can happen first.
ah, i see - thanks
I notice I wrongly wrotef((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.
"Compiler programmers aren't evil" Exceptclang which seems to have been programmed bySkeletor. Doing unexpectedly evil things like generating half an executable but stopping halfways upon UB.
Should I expect compiler warnings like,Nyehehehehehehehhhhh! You boobs! You nincompoops! Why am I surrounded by such utter incompetents?
After reading you answer I'm still not sure whyf((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?
We are in UB land, so no guaranty. I speculate on some implementation which might returnf(2, 2) with some logic.result_of_side_effect andi might be seen as the same.
Undefined Behavior was intended to be awaiver of jurisdiction that was agnostic with regard to behavioral precedents, rather than an invitation to gratuitously defenestrate them. The Standards never really made any systematic effort to identify all precedents which most implementations supported, and upon which non-trivial amounts of existing code relied, but merely those where the authors thought implementations might otherwise go against precedent.
do you mean UB or uB? is it undefined or unspecified?
undefined behavior, sof(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).
I doesn't address the OP's question of why the compiler would suddenly pass the value after i++ and not the one before i++. The sequence: old_i = i ; (side effect) ; call f(old_i, whatever) has not reason to be replaced with old_i = i ; (side effect) ; call f(new_i, whatever). The question is why is f(2,2) even mentioned ?
I second that. The answer should rather stop at "Speculating on UB (undefined behavior) is mostly pointless.", what comes after doesn't explain the logic behindf(2,2) anyway
|
5

This 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:

  1. Performnew Widget
  2. Callbar()
  3. Call the constructor ofstd::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.

answeredyesterday
GKxx's user avatar

2 Comments

If 1, 3, 2, 4 leads to f(2, 2), what order of operations lead to not having 2, 2? I'm a bit confused as to how incrementing after "Store the value of i as the first argument" changes the first argument's value.
I fail to see how the order "1, 3, 2, 4" will lead tof(2,2), as this answer claims. As far as I can tell, it will rather lead tof(1, 1).
-3

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.

answered20 hours ago
CSM's user avatar

4 Comments

There is no "C and C++ calling convention". Calling convention is platform-specific. And what if the args are passed in registers, not pushed onto the stack? In fact there's no necessary connection between the evaluation order of the arguments and the "calling convention" (the order in which they are pushed onto the stack).
It is clear that with this assembly code, replacing j++ by i we get f(1,1);
All modern mainstream platforms pass the first few args in registers, although yes in a debug build the compiler may still evaluate them right-to-left so the first arg is the last one computed. (The closest to mainstream that only uses stack args is 32-bit x86 with legacy calling conventions. That's been obsolete for a decade or two.) But more importantly, arg-passing order on the stack doesn't constrain the order of evaluation, only the abstract-machine rules do. The compilers people use optimize aggressively, they don't just transliterate C to asm straightforwardly.
In C and C++ the caller removes the arguments to the stack - If you're still talking about 32-bit x86, there are callee-pops conventions on Windows, such asstdcall 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.)

Your Answer

Sign up orlog in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

By clicking “Post Your Answer”, you agree to ourterms of service and acknowledge you have read ourprivacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.