Boost logo

Boost :

From: Peter Dimov (pdimov_at_[hidden])
Date: 2022-02-18 16:54:29


Mikhail Kremniov wrote:
> Hi,
>
> Initially, I posted this question to the boost-users list, but then I was advised to
> repost it here.

Hello, and thanks for that, as this is the proper list for this question.

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

The standard has changed several times with respect to these lifetime issues,
and I'm not sure launder is useful for anything today. But I don't quite know
for sure.

We have three potential problems in Optional, two of which are related to
launder and one that isn't. First, there's the object replacement issue. If we
have (for demonstration purposes only)

struct X { int m; };
optional<X> opt( X{1} );
X const& rx = *opt;
opt.reset();
opt.emplace( X{2} );
std::cout << rx.m << std::endl;

the C++14 standard says we're fine as long as X doesn't contain const or
reference members, and the C++17 standard used to say the same thing,
but was changed before publication as a result of a national body comment
to be less restrictive and we're fine there as long as the entire X object isn't
const.

So this would be undefined

struct X { int m; };
optional<const X> opt( X{1} );
X const& rx = *opt;
opt.reset();
opt.emplace( X{2} );
std::cout << rx.m << std::endl;

without laundering _unless_ optional strips const before doing the placement
new (I haven't checked whether it already does). So that's one use of launder
taken care of.

Second, there's the pointer provenance issue with placement new. If we have

alignas(X) unsigned char storage[sizeof(X)];
X* p1 = (X1*)storage;
X* p2 = new(p1) X{1};

cppreference says we can't use p1 and need to use launder(p1). But I'm not so
sure about that.

The exact same thing happens in std::vector. When you do push_back in
vector<X>, an object of type X is created using placement new, but the result
of new isn't stored anywhere. Instead, in op[] for instance, the "old" pointer
is returned.

And I don't see `launder` anywhere in the libstdc++ `<vector>`. I haven't
checked the others but I suspect they don't have it either.

So that's the second potential use of launder.

The third issue we're having is with our aligned_storage. Its address() member
function doesn't return the address of the unsigned char[] array, but the address
of the aligned_storage object itself (or rather, to its aligned_storage_impl base,
which contains the char array as its first member. Inside a union.)

This means that we aren't in the clear with respect to the "provides storage"
wording in https://eel.is/c++draft/basic.memobj#intro.object-3. I'm not entirely
sure that what we're doing is undefined, but it looks like we can avoid this issue
by just returning the address of the char array in address().

Note that this potential source of UB can't be fixed with launder.

> 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).
>
> So, can switching from -std=c++14 to -std=c++17 be a breaking change when
> using Boost? The fact that Boost.Variant and Boost.Optional don't use
> std::launder - is it an oversight or a conscious decision?

In principle, C++17 doesn't introduce any new UB that C++14 already didn't have.
It's theoretically possible for compilers to start doing more aggressive
optimizations in C++17 mode and above, but I don't think they do. Not sure what
LLVM plans are, though.


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