Learner's Notes - Introduction to std::launder
Introduction
Lets start with what std::launder
is. According to cppreference, it has the folllowing function definition:
1
2
template <class T>
constexpr T* launder( T* p ) noexcept;
OK, so it takes a pointer to type T
and returns a pointer to type T
, but what does it do specifically? The informal definition given is the following:
Provenance fence with respect to
p
. Returns a pointer to the same memory thatp
points to, but where the referent object is assumed to have a distinct lifetime and dynamic type.
So it takes in a pointer p
and returns it? Isn’t that just a no-op? Thus begins our journey to understand the mysterious std::launder
.
Placement New
Before we begin, it would be useful to know a little bit about placement new
. All that is needed to know is that it is used to construct objects in pre-allocated storage. We can see an example given by cppreference
1
2
3
4
5
6
7
8
9
10
11
12
// within any block scope...
{
// Statically allocate the storage with automatic storage duration
// which is large enough for any object of type “T”.
alignas(T) unsigned char buf[sizeof(T)];
T* tptr = new(buf) T; // Construct a “T” object, placing it directly into your
// pre-allocated storage at memory address “buf”.
tptr->~T(); // You must **manually** call the object's destructor
// if its side effects is depended by the program.
} // Leaving this block scope automatically deallocates “buf”.
Initial Problem with std::optional
In section 6.8.8.3 of the C++17 standard, note the following:
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:
[…]
the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type
[…]
Let’s take a look at the simplified code of original problem brought up here:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <utility>
template <class T>
union U {
constexpr U(T &&x) : value_(std::forward<T>(x)) {}
unsigned char dummy_;
T value_;
};
template <class T>
struct optional {
constexpr optional(T &&x) : storage_(std::forward<T>(x)) {}
template <class... Params>
void emplace(Params &&...params) {
storage_.value_.~T();
new (&storage_.value_) T(std::forward<Params>(params)...);
}
constexpr T &operator*() & noexcept { return storage_.value_; }
U<T> storage_;
};
struct A {
constexpr A(int &x) : ref(x) {}
int &ref;
};
int main() {
int n1{0}, n2{0};
optional<A> opt2{A{n1}};
opt2.emplace(n2);
(*opt2).ref = 1;
std::cout << n1 << " " << n2 << std::endl;
}
So, what’s the crux of the issue? Look at line 19, specifically return storage_.value_;
. Note that struct A
contains an int&
member. From the standard mentioned above, this means the name of the original object is not guaranteed to automatically refer to the new object, leading to undefined behavior on line 32.
std::launder
std::launder
is designed to overcome this issue (among other issues that we’ll discuss later). By using std::launder
, we can avoid undefined behavior by modifying line 19 to constexpr T &operator*() & noexcept { return *std::launder(&storage_.value_); }
.
Formally, std::launder
does the following:
Given:
- the pointer
p
represents the addressA
of a byte in memory- an object
x
is located at the addressA
x
is within its lifetime- the type of
x
is the same asT
, ignoring cv-qualifiers at every level- every byte that would be reachable through the result is reachable through
p
(bytes are reachable through a pointer that points to an objecty
if those bytes are within the storage of an objectz
that is pointer-interconvertible withy
, or within the immediately enclosing array of whichz
is an element).Then
std::launder(p)
returns a value of typeT*
that points to the objectx
. Otherwise, the behavior is undefined.
Simply put, std::launder
ensures that the resulting pointer will point to the newly created object, thereby eliminating undefined behavior.
C++20 Changes
With C++20, there are some notable changes, including the conditions under which the name of the original object automatically refers to the new object:
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object.
The concept of transparently replaceable has evolved:
An object
x
is transparently replaceable by an objecty
if:
- the storage that
y
occupies exactly overlays the storage thatx
occupied, andx
andy
are of the same type (ignoring the top-level cv-qualifiers), andx
is not a complete const object, and- neither
x
nory
is a potentially-overlapping subobject, and- either
x
andy
are both complete objects, orx
andy
are direct subobjects of objectsox
andoy
, respectively, andox
is transparently replaceable byoy
.
Notably, it no longer requires that a class type must not have non-static data members whose type is const-qualified or a reference type. This means that, even without std::launder
, there are no issues with the previously given example.
So, is std::launder
just a relic of the past?
Another std::launder issue
Even though the notion of transparently replaceable has evolved, there are still limitations, one of them being:
An object
x
is transparently replaceable by an objecty
if:
- […]
x
andy
are of the same type (ignoring the top-level cv-qualifiers), and- […]
Here is a simple example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstddef>
#include <iostream>
#include <new>
struct T {
int val;
T(int val): val{val} {}
};
int main() {
alignas(T) std::byte data[sizeof(T)];
new (data) T{1};
T* p = reinterpret_cast<T*>(data);
std::cout << p->val << std::endl; // UB
}
Since data
does not directly point to an object of type T
, there is no guarantee that p
will point to the newly created object, leading to undefined behavior on line 14. There are two ways to solve this issue:
- Use the pointer returned from placement
new
:
Simply changing line 12 and 13 toT* p = new (data) T{1};
would solve the issue. However, this may not always be a viable solution due to the overhead of storing the pointer returned by placementnew
. - Use
std::launder
:
Changing line 13 toT* p = std::launder(reinterpret_cast<T*>(data));
also avoids undefined behavior, ensuring thatp
correctly points to the newly created object within data, avoiding any undefined behavior.
Conclusion
Don’t ask. If you’re not one of the 5 or so people in the world who already know what this is, you don’t want or need to know.
These are the wise words of Botond Ballo, and I have to agree with him. However, I do hope this post provides some insights into what std::launder
is and what it tries to solve.
Till next time!
References
- On launder()
- Implementability of std::optional
- cppreference - Lifetime
- Working Draft, Standard for Programming Language C++ (C++17)
- Working Draft, Standard for Programming Language C++ (C++20)
- std::launder: the most obscure new feature of C++17
- What is the purpose of std::launder?
- std::launder use cases in C++20
- Placement new + reinterpret_cast in C++14: well-formed?