8

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.

Adrian
  • 987
  • 5
  • 13
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/216946/discussion-on-question-by-adrian-were-all-implementations-of-stdvector-non-por). – Samuel Liew Jun 30 '20 at 13:10
  • Somewhat related question and answer, [c++ - Does reinterpret_casting std::aligned_storage* to T* without std::launder violate strict-aliasing rules? - Stack Overflow](https://stackoverflow.com/questions/47735657/does-reinterpret-casting-stdaligned-storage-to-t-without-stdlaunder-violat?noredirect=1&lq=1) – user202729 Feb 25 '22 at 14:10

1 Answers1

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

F.v.S.
  • 167
  • 1
  • 8
  • What if `value_type` doesn't satisfy requirements for implicit lifetime start? https://en.cppreference.com/w/cpp/language/lifetime – HolyBlackCat Dec 21 '21 at 11:21
  • 1
    @HolyBlackCat 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". – F.v.S. Dec 22 '21 at 01:30
  • Oh! So the lifetime of the array always starts, even if the lifetime of the elements doesn't? – HolyBlackCat Dec 22 '21 at 07:07
  • 1
    Yes. This is required for the `allocate` function of an allocator. – F.v.S. Dec 22 '21 at 07:33