When emplace_back()
is called on std::vector
instance, an object is created in a previously allocated storage. This can be easily achieved with placement-new, which is perfectly portable.
But now, we need to access the emplaced element without invoking undefined behavior.
From this SO post I learned that there are two ways of doing this
use the pointer returned by placement-new:
auto *elemPtr = new (bufferPtr) MyType();
or, since C++17, std::launder the pointer casted from
bufferPtr
auto *elemPtr2 = std::launder(reinterpret_cast<MyType*>(bufferPtr));
The second approach can be easily generalized to the case, where we have a lot of objects
emplaced in adjacent memory locations, as in std::vector
. But what people did before C++17?
One solution would be to store pointers returned by placement-new in a separate dynamic array.
While this is certainly legal, I don't think it really implements std::vector [besides, it's a crazy idea to separately store all the addresses that we know already].
The other solution is to store lastEmplacedElemPtr
inside std::vector
, and remove an appropriate integer from it -- but since we don't really have an array of MyType
objects this is probably also undefined. In fact, an example from this cppreference page claims that if we have two pointers
of the same type that compare equal, and one of them can be dereferenced safely, dereferencing the other can be still undefined.
So, was there a way to implement std::vector in a portable way before C++17? Or maybe std::launder is indeed a crucial piece of C++ when it comes to placement-new, that was missing since C++98?
I'm aware that this question is superficially similar to a lot of other questions on SO, but as far as I can tell none of them explains how to legally iterate over objects constructed by placement-new. In fact, this is all a bit confusing. For instance comments in the example
form cppreference documentation of std::aligned_storage
seem to suggest that there has been some change between C++11 and C++17, and a simple
aliasing-violating reinterpret_cast
was legal before C++17 [without the need for std::launder
]. Similarly, in the example from documentation of std::malloc
they simply do a pointer arithmetic on a pointer returned by std::malloc
(after static_cast
to the correct type).
By contrast, according to the answer to this SO question when it comes to placement-new and reinterpret_cast
:
There have been some significant rule clarifications since C++11 (particularly [basic.life]). But the intent behind the rules hasn't changed.