It's safe if you did a s.reserve(s.size() + 1)
before so that a reallocation is guaranteed not to happen.
It's undefined behavior if you don't know the current capacity and thus can't control if there will be a relocation and therefore also iterator as well as reference and value invalidation at an unspecified point in time.
Why this is a problem, can be simply traced down to the lifecycle of the object referenced by s.back()
. Unless specified explicitly otherwise, you must ensure that the lifecycle of an object exceeds that of the last used reference. You did not get any guarantees beyond that, and therefore you were required to keep the argument of s.push_back(const T&)
alive until push_back
returns control to you, and you can not make any assumption about when or why push_back
would access the object.
There are edge cases (such as: non-trivial but noexcept
move constructor, throwing copy constructor) where other constraints such as the strong exception guarantees don't permit a use-after-free due to the specific order of events in the implementation, but those constraints are easily lifted by as little as e.g. a noexcept
copy constructor which permits the worst case of move before copy again. Also, that's still all implementation defined.
s.push_back(auto(s.back()));
on the other hand is safe again (even though it's using another signature, and is using both move and copy constructor with a temporary).
s.push_back(std::ref(auto(s.back())));
ensures that the arguments lifecycle is correct, even though you generally shouldn't use it, as it requires 2 invocations of the copy constructor.
push_back
, but the committee is clear that s.emplace(s.begin(), s.back()) must work. – Tripinnate