lundi 29 juin 2020

Were all implementations of std::vector non-portable before std::launder?

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

  1. use the pointer returned by placement-new: auto *elemPtr = new (bufferPtr) MyType();

  2. 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.

Aucun commentaire:

Enregistrer un commentaire