I have the following code:
#include <vector>
#include <iostream>
std::vector <int> a;
int append(){
a.emplace_back(0);
return 10;
}
int main(){
a = {0};
a[0] = append();
std::cout << a[0] << '\n';
return 0;
}
The function append()
as a side effect increases the vector size by one. Because of how vectors work, this can trigger a reallocation of its memory when exceeding its capacity.
So, when doing a[0] = append()
, if a reallocation happens, then a[0]
is invalidated and points to the old memory of the vector. Because of this you can expect the vector to end up being {0, 0}
instead of {10, 0}
, because it is assigning to the old a[0]
instead of the new one.
The weird thing that confuses me is that this behavior changes between C 14 and C 17.
On C 14, the program will print 0. On C 17, it will print 10, meaning a[0]
actually got 10 assigned to it. So, I have the following questions whose answers I couldn't find:
- Is C 17 evaluating
a[0]
's memory address after evaluating the RHS of the assignment expression? Does C 14 evaluate this before and that's why it changes? - Was this a bug that was fixed in C 17? What changed in the standard?
- Is there a clean way to make this assignment behave like in C 17 when using C 14 or C 11?
CodePudding user response:
This code is UB prior to C 17, as pointed out in comments, due to C evaluation order rules. The basic problem: Order of operations is not order of evaluation. Even something like x x
is UB.
In C 17, sequencing rules for assignments were changed:
- In every simple assignment expression E1=E2 and every compound assignment expression E1@=E2, every value computation and side-effect of E2 is sequenced before every value computation and side effect of E1
CodePudding user response:
The program has undefined behavior prior to C 17 because the evaluation order is unspecified and one of the possible choices leads to using an invalidated reference. (That’s how undefined behavior works: even if you log the two evaluations and the right hand side logs first, it might have evaluated the other way with the effect of the undefined behavior being the incorrect logging.)
While it’s an informal point, the change in C 17 to specify the order of evaluation for certain operators (including =
) wasn’t considered to be a bug fix, which is why compilers don’t implement the new rules in prior language modes. (It’s the code that’s broken, not the language.)
Cleanliness is subjective, but the usual way to deal with an ordering issue like this is to introduce a temporary variable:
{ // limit scope
auto x=append();
a[0]=x; // with std::move for non-trivial types
}
This occasionally interferes with passing by value to the assignment operator, which can’t be helped.