From Undefined to Defined: Using std::launder in C++

In today's post, I will continue with the overall topics of the last two months. Today you'll learn when and where you need to apply C++17's std::launder and where the difference to this utility is to reinterpret_cast or std::start_lifetime_as.

The fields where you can apply today's learning vary. The embedded domain is usually one where std::launder is used, but if you're writing library code, laundering occurs as well.

When things may break

I'm using the example from the paper P0532R0:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct X {
  const int n;  A 
  double    d;
};

X* p = new X{7, 8.8};  B 

new(p) X{42, 9.9};  C 

int  i = p->n;  D 
auto d = p->d;  E 

You're looking at several different pieces that need to come together. Notice that the struct X declares the member n as const.

Next, with the help of new, an object is created in B and the resulting pointer is stored in p. So far, so good.

The interesting part starts next in C with the placement new. If you've never done this, great, then you might not need to do the laundry, at least not in your C++ code.

The placement new itself is also fine. The issue occurs later in D and E. Here is access to the values of the pointer p. But the compiler is allowed to assume that after B the contents of p are unchanged, as p itself was never used to change the contents of the object. Even worse, one of Xs data members is const. This is a free ticket for the optimizer to rightfully assume that the value of n never changes after construction.

But this is precisely what I've done in C. I change a const value! The values of i and d are unknown. You're looking at undefined behavior.

The undefined behavior in the previous example can be avoided easily, even without laundering, by updating the pointer and those telling the compiler that the values behind p have changed:

1
2
3
4
5
6
X* p = new X{7, 8.8};  B 

p = new(p) X{42, 9.9};  C 

int  i = p->n;  D 
auto d = p->d;  E 

So the question is, why not simply do that and forget about std::launder? Well, all the time you can do that, please do, and forget about std::launder.

Unfortunately, there are times when updating the pointer would sacrifice precious resources.

When things get more complicated

Suppose you implement a custom allocator like below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
template<size_t SIZE, size_t ALIGNMENT>
class Buffer {
  alignas(ALIGNMENT) std::byte mBuffer[SIZE];

public:
  template<typename T, typename... Ts>
  T* Construct(Ts... vals)
  {
    new(mBuffer) T{std::forward<Ts>(vals)...};
    return reinterpret_cast<T*>(mBuffer);
  }

  template<typename T>
  [[nodiscard]] T* Get()
  {
    return reinterpret_cast<T*>(mBuffer);
  }
};

You're looking at two functions that the allocator provides: Construct and Get. The concept of Buffer is that the data stored is type erased. A possible simplified usage might be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct Point {  A 
  int x;
  int y;
};

struct Point3D {
  int x;
  int y;
  int z;
};

std::array<Buffer<12, 8>, 2> storage{};  B 

C Init
storage.at(0).Construct<Point>(2, 3);
storage.at(1).Construct<Point3D>(4, 5, 6);

D Start usage
storage.at(0).Get<Point>()->x = 7;

You see two data types, Point and Point3D as an example for arbitrary types in A. storage represents stack memory, which can store arbitrary data. As long as users know which datatype is behind an index (and said type isn't too large in capacity), everything can be stored.

At the first element of the array C, I construct a Point object, while at the second element I create a Point3D. It is only later in the program D that the objects are actually used. Yet nobody stored a pointer to the freshly constructed objects for a good reason. Such a pointer would require storage capacity, and you already know where this pointer points .

Book an in-house C++ training class

Do you like this content?

Here is your next chance joining my class:
  • Modern C++: When Efficiency Matters @CppCon
  • September 09 - 11, 2026, - UTC
Book your seat now!

From the abstract machine perspective, I poked holes into the type system. The compiler can rightfully assume that the data behind Get does not change unless it sees a direct write access D, for example. Such an access goes unnoticed by the compiler (or may go since it is undefined behavior) in the code below in E, when I create a fresh Point object at an existing location.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
std::array<Buffer<12, 8>, 2> storage{};  B 

C Init
storage.at(0).Construct<Point>(2, 3);

D Start usage
storage.at(0).Get<Point>()->x = 7;

E Update
storage.at(0).Construct<Point>(8, 9);

Obviously, the easy way to avoid issues here is to store the pointer returned by each Construct call. If that isn't feasible, as here, then the correct thing to do is to launder the pointer with std::launder. This special utility acts as a devirtualization fence, inhibiting compiler optimizations and assumptions.

std::launder to the rescue

Let's update the Buffer implementation in a safe manner. First, Construct, my initial implementation was:

1
2
3
4
5
6
template<typename T, typename... Ts>
T* Construct(Ts... vals)
{
  new(mBuffer) T{std::forward<Ts>(vals)...};
  return reinterpret_cast<T*>(mBuffer);
}

You can make this code safe, even without std::launder by returning the resulting pointer from new:

1
2
3
4
5
template<typename T, typename... Ts>
T* Construct(Ts... vals)
{
  return new(mBuffer) T{std::forward<Ts>(vals)...};
}

The second function you need to fix is Get, this time by adding std::launder:

1
2
3
4
5
template<typename T>
[[nodiscard]] T* Get()
{
  return std::launder(reinterpret_cast<T*>(mBuffer));
}

The code now assumes that you call Construct before you ever invoke Get. Those mBuffer contains a valid object, and because you never make mistakes, the object correlates with the one that was created with Construct.

Summary

With std::launder you can update a pointer that points to an already existing object at that memory location. So the object's lifetime there has already been started.

Adding std::launder to the summary table I shared in my previous post, the table becomes:

Conversion utilityWhen to useStandard
reinterpret_castConverting the pointer typeC++98
std::bit_castObtaining the bit representation of an objectC++20
std::start_lifetime_asStarting the lifetime of an object in a chunk of memoryC++23
std::launderUpdate pointer view for an already in-lifetime objectC++17

Andreas

Recent posts