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.