Boost logo

Boost :

From: Andrey Semashev (andrey.semashev_at_[hidden])
Date: 2022-02-18 14:37:32


On 2/18/22 13:33, Mikhail Kremniov via Boost wrote:
> Hi,
>
> Initially, I posted this question to the boost-users list, but then I
> was advised to repost it here.
>
> I wonder, what is the Boost community's opinion of std::launder? In
> particular, of its necessity when accessing an object that was created
> via placement-new in an aligned_storage. As I can see, it's used by
> Boost.Beast in its implementation of the variant type, but not in other
> parts of Boost.
> The reason I'm asking this is that I'm working on a C++14 code-base that
> uses Boost.Variant and Boost.Optional extensively (as well as other
> parts of Boost that use Variant and Optional internally). Now we're
> trying to switch to C++17 at least, and I worry whether it could
> potentially break things.
>
> To put things into context, C++ standard states in the [basic.life]
> section that if you had an object and then created a new one in the same
> location, you can use the pointer to the old object to refer to the new
> one only if (among other things) "the new object is of the same type as
> the original object (ignoring the top-level cv-qualifiers)".
>
> This means, as I understand it, that it's technically illegal to
> reinterpret_cast the pointer to aligned_storage to the pointer to the
> actual type of the object created via placement-new, because
> placement-new has already ended the lifetime of the aligned_storage
> object. So, implementations of Boost.Optional and Boost.Variant are
> illegal then (?). But it's been like that since C++03 and Boost worked
> fine all that time, so it looks like this UB existed only "on paper".

Casting a pointer is not illegal. Accessing the pointed object is, if
the object does not actually exist at the pointed location. That is, if
at the aligned_storage location an object of type T was constructed
(through any means described by the standard), you can cast the pointer
to aligned_storage to a pointer to T and dereference it, and this is ok.
If such an object does not exist, then dereferencing the pointer is UB.

> But C++17 then added "Note: If these conditions are not met, a pointer
> to the new object can be obtained from a pointer that represents the
> address of its storage by calling std::launder".
> So now the language has the ability to deal with that UB, and a question
> arises, is it possible that compilers could start to use the UB to
> perform additional optimizations and make it a real UB?
> Also, I've seen a couple of times on stackoverflow.com people saying
> that it's actually fine to reinterpret_cast the storage in C++14, but in
> C++17 it's not (they didn't explain why though).

What std::launder is intended to be is a fence for compiler speculations
as to the possible value in the dereferenced location. That is, in the
following example:

  alignas(int) unsigned char storage[sizeof(int)];
  int* p1 = new (storage) int(1);
  float* p2 = new (storage) float(2.0f);
  int* p3 = new (storage) int(3);
  std::printf("%d\n", *p1);

the program is allowed to print 1 because the compiler may speculate
that this code does not modify the value pointed to by p1. Adding this
line immediately before printf:

  p1 = std::launder(p1);

ensures that the compiler "forgets" whatever assumptions it had about
the value pointed to by p1, so printf has to reload the value (or, at
least, the compiler has to re-evaluate its assumptions given that the
value could have been modified through other means).

Note that p3 in this example is guaranteed to point to the int of 3
regardless of the launder.

Given the above, I don't think Boost.Optional or Boost.Variant are
affected. For both of these components, pointers and references to the
stored value are invalidated if the value is destroyed (e.g. by
resetting boost::optional or changing the current value type of
boost::variant). It is also illegal (or simply not possible) to obtain a
pointer or reference to the value that doesn't exist in the internal
storage.

  boost::optional<int> opt;
  opt = 1;
  int* p1 = &*opt;
  opt = boost::none; // makes p1 invalid
  opt = 2; // p1 is still invalid
  p1 = &*opt; // now p1 points to 2

std::launder may be useful to the users of these components, if one
keeps around a pointer to the stored object without re-obtaining it from
boost::optional or boost::variant. But frankly, I don't see the point,
as obtaining the pointer is a trivial and logical thing to do anyway.


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk