'What is the purpose of std::launder?
P0137 introduces the function template std::launder
and makes many, many changes to the standard in the sections concerning unions, lifetime, and pointers.
What is the problem this paper is solving? What are the changes to the language that I have to be aware of? And what are we launder
ing?
Solution 1:[1]
std::launder
is aptly named, though only if you know what it's for. It performs memory laundering.
Consider the example in the paper:
struct X { const int n; };
union U { X x; float f; };
...
U u = {{ 1 }};
That statement performs aggregate initialization, initializing the first member of U
with {1}
.
Because n
is a const
variable, the compiler is free to assume that u.x.n
shall always be 1.
So what happens if we do this:
X *p = new (&u.x) X {2};
Because X
is trivial, we need not destroy the old object before creating a new one in its place, so this is perfectly legal code. The new object will have its n
member be 2.
So tell me... what will u.x.n
return?
The obvious answer will be 2. But that's wrong, because the compiler is allowed to assume that a truly const
variable (not merely a const&
, but an object variable declared const
) will never change. But we just changed it.
[basic.life]/8 spells out the circumstances when it is OK to access the newly created object through variables/pointers/references to the old one. And having a const
member is one of the disqualifying factors.
So... how can we talk about u.x.n
properly?
We have to launder our memory:
assert(*std::launder(&u.x.n) == 2); //Will be true.
Money laundering is used to prevent people from tracing where you got your money from. Memory laundering is used to prevent the compiler from tracing where you got your object from, thus forcing it to avoid any optimizations that may no longer apply.
Another of the disqualifying factors is if you change the type of the object. std::launder
can help here too:
alignas(int) char data[sizeof(int)];
new(&data) int;
int *p = std::launder(reinterpret_cast<int*>(&data));
[basic.life]/8 tells us that, if you allocate a new object in the storage of the old one, you cannot access the new object through pointers to the old. launder
allows us to side-step that.
Solution 2:[2]
std::launder
is a mis-nomer. This function performs the opposite of laundering: It soils the pointed-to memory, to remove any expectation the compiler might have regarding the pointed-to value. It precludes any compiler optimizations based on such expectations.
Thus in @NicolBolas' answer, the compiler might be assuming that some memory holds some constant value; or is uninitialized. You're telling the compiler: "That place is (now) soiled, don't make that assumption".
If you're wondering why the compiler would always stick to its naive expectations in the first place, and would need to you to conspicuously soil things for it - you might want to read this discussion:
Why introduce `std::launder` rather than have the compiler take care of it?
... which lead me to this view of what std::launder
means.
Solution 3:[3]
I think there are two purposes of std::launder
.
- A barrier for
constant folding/propagation, includingdevirtualization. - A barrier for fine-grained object-structure-based alias analysis.
Barrier for overaggressive constant folding/propagation (abandoned)
Historically, the C++ standard allowed compilers to assume that the value of a const-qualified or reference non-static data member obtained in some ways to be immutable, even if its containing object is non-const and may be reused by placement new.
In C++17/P0137R1, std::launder
is introduced as a functionality that disables the aforementioned (mis-)optimization (CWG 1776), which is needed for std::optional
. And as discussed in P0532R0, portable implementations of std::vector
and std::deque
may also need std::launder
, even if they are C++98 components.
Fortunately, such (mis-)optimization is forbidden by RU007 (included in P1971R0 and C++20). AFAIK there's no compiler performing this (mis-)optimization.
Barrier for devirtualization
A virtual table pointer (vptr) can be considered constant during the lifetime of its containing polymorphic object, which is needed for devirtualization. Given that vptr is not non-static data member, compilers is still allowed to perform devirtualization based on the assumption that the vptr is not changed (i.e., either the object is still in its lifetime, or it is reused by a new object of the same dynamic type) in some cases.
For some unusual uses that replace a polymorphic object with a new object of different dynamic type (shown here), std::launder
is needed as a barrier for devirtualization.
IIUC Clang implemented std::launder
(__builtin_launder
) with these semantics (LLVM-D40218).
Barrier for object-structure-based alias analysis
P0137R1 also changes the C++ object model by introducing pointer-interconvertibility. IIUC such change enables some "object-structure-based alias analysis" proposed in N4303.
As a result, P0137R1 makes the direct use of dereferencing a reinterpret_cast
'd pointer from an unsigned char [N]
array undefined, even if the array is providing storage for another object of correct type. And then std::launder
is needed for access to the nested object.
This kind of alias analysis seems overaggressive and may break many useful code bases. AFAIK it's currently not implemented by any compiler.
Relation to type-based alias analysis/strict aliasing
IIUC std::launder
and type-based alias analysis/strict aliasing are unrelated. std::launder
requires that an living object of correct type to be at the provided address.
However, it seems that they are accidently made related in Clang (LLVM-D47607).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Community |
Solution 2 | |
Solution 3 | F.v.S. |