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

1

10

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.

Lardaceous answered 29/6, 2020 at 16:25 Comment(2)
Comments are not for extended discussion; this conversation has been moved to chat.Thuggee
Somewhat related question and answer, c++ - Does reinterpret_casting std::aligned_storage* to T* without std::launder violate strict-aliasing rules? - Stack OverflowStreamy
R
1

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

Rafaelle answered 21/12, 2021 at 10:15 Comment(4)
What if value_type doesn't satisfy requirements for implicit lifetime start? en.cppreference.com/w/cpp/language/lifetimeHintze
@Hintze The lifetime of any element doesn't automatically begin, and hence placement new or construct_at call is needed to start its lifetime. IIUC P0593 has permitted the situation that "the lifetime of a complete object implicitly begins, while its subobjects do not".Rafaelle
Oh! So the lifetime of the array always starts, even if the lifetime of the elements doesn't?Hintze
Yes. This is required for the allocate function of an allocator.Rafaelle

© 2022 - 2024 — McMap. All rights reserved.