A not-so-obvious caveat when using emplace_back()
To reproduce the issue i recently came across, here is the smallest self-contained example showing the problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct Sample { int value; int * alias; explicit Sample (int val = 0 ) : value(val), alias(&value) { } }; std::ostream& operator <<(std::ostream& os, const Sample& sample) { os << "{ value=" << sample.value << " , alias=" << sample.alias << " , &value=" << &sample.value << " }" ; return os; } int main () { std::vector<Sample> vec; vec.emplace_back (1 ); std::cout << "after 1st emplace: vec[0] " << vec[0 ] << "\n" ; vec.emplace_back (2 ); std::cout << "after 1st emplace: vec[0] " << vec[0 ] << "\n" ; std::cout << "*alias = " << *(vec[0 ].alias) << "\n" ; }
output:
1 2 3 after 1st emplace: vec[0] { value=1 , alias=0x5a1202d0d2b0 , &value=0x5a1202d0d2b0 } after 1st emplace: vec[0] { value=1 , alias=0x5a1202d0d2b0 , &value=0x5a1202d0e2e0 } *alias = -1591726835
The issue is: When calling emplace_back()
, memory will be reallocated to fit more data, and elements will be copied/moved to new addresses. However, float* alias
is just copied/moved as a raw address, meaning its value will not be modified.
To fix this issue, we can explicitly override the default copy/move constructors that compilers generate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 struct Sample { int value = 0 ; int * alias = &value; Sample () = default ; explicit Sample (int v) : value(v), alias(&value) { } Sample (const Sample& rhs) : value (rhs.value), alias (&value) {} Sample& operator =(const Sample& rhs) { value = rhs.value; alias = &value; return *this ; } Sample (Sample&& rhs) noexcept : value (rhs.value), alias (&value) {} Sample& operator =(Sample&& rhs) noexcept { value = rhs.value; alias = &value; return *this ; } }; std::ostream& operator <<(std::ostream& os, const Sample& sample) { os << "{ value=" << sample.value << " , alias=" << sample.alias << " , &value=" << &sample.value << " }" ; return os; } int main () { std::vector<Sample> vec; vec.emplace_back (1 ); std::cout << "after 1st emplace: vec[0] " << vec[0 ] << "\n" ; vec.emplace_back (2 ); std::cout << "after 1st emplace: vec[0] " << vec[0 ] << "\n" ; std::cout << "*alias = " << *(vec[0 ].alias) << "\n" ; }
output:
1 2 3 after 1st emplace: vec[0] { value=1 , alias=0x5eee05be82b0 , &value=0x5eee05be82b0 } after 1st emplace: vec[0] { value=1 , alias=0x5eee05be92e0 , &value=0x5eee05be92e0 } *alias = 1
It’s not just emplace_back
: operations such as push_back
, insert
, emplace
, and resize
can also expose the issue if they involve rearranging the container’s data.