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.
IIUC after P0593R6 and P1971R0/RU007, both merged into C++20 and considerable as defect reports against previous revisions, a portable std::vector
implementation doesn't need std::launder
.
First, after the allocate
function of the given allocator (which might call operator new
, std::malloc
, or something like them) returned, a value_type[N]
array (where N
is equal to the requested number passed to allocate
) is implicitly created within the allocated storage (thanks to P0593R6), and thus pointer arithmetic is valid. Even if the elements may be unconstructed.
Second, when we use placement-new without std::launder
, the constructed objects can be treated as array elements, as new objects transparently replace the array elements even if the element type has const/reference non-static data members (thanks to P1971R0/RU007 and subsequent fixes in P2103R0/US041).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With