#include <iostream>
#include <new>
struct A {
int const n;
void f() {
new (this) A{2};
}
void g() {
std::cout << this->n;
}
void h() {
std::cout << std::launder(this)->n;
}
};
int main() {
auto a = A{1};
a.f();
std::cout << std::launder(&a)->n; // This is guaranteed to print 2.
a.h(); // This is guaranteed to print 2.
a.g(); // Is it guaranteed to print 2?
std::cout << a.n; // Is it guaranteed to print 2?
new (&a) A{3};
std::cout << std::launder(&a)->n; // This is guaranteed to print 3.
a.h(); // This is guaranteed to print 3.
a.g(); // Is it guaranteed to print 3?
std::cout << a.n; // Is it guaranteed to print 3?
}
Consider the following excerpt from another answer:
The compiler is allowed to assume that a truly const variable (not merely a const&, but an object variable declared const) will never change.
std::cout << std::launder(&a)->n;
is guaranteed to behave as expected, though, it is too tedious and too ugly, especially for this
pointers in the member function body.
Does C++23 guarantee std::cout << a.n;
and std::cout << this->n;
behave as expected even if without std::launder
?
All of these cases are well-defined (even without std::launder
) in this case because an object of type A
is transparently replaceable by another object of type A
.
This is covered by:
[basic.life (8)]
(8) 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.
An objecto1
is transparently replaceable by an objecto
2 if:
(8.1) the storage thato2
occupies exactly overlays the storage thato1
occupied, and
(8.2)o1
ando2
are of the same type (ignoring the top-level cv-qualifiers), and
(8.3)o1
is not a const, complete object, and
(8.4) neithero1
noro2
is a potentially-overlapping subobject ([intro.object]), and
(8.5) eithero1
ando2
are both complete objects, oro1
ando2
are direct subobjects of objectsp1
andp2
, respectively, andp1
is transparently replaceable byp2
.
In this case:
sizeof(A) == sizeof(A)
, so all bytes are overlayedo1
and o2
are of the same type (ignoring the top-level cv-qualifiers)A
and A
are the same typeo1
is not a const, complete objectauto a = A{1};
a
is not const in this exampleo1
nor o2
is a potentially-overlapping subobjectA
is neither a baseclass of A
nor a no_unique_address
member of A
([intro.object (7)])o1
and o2
are both complete objects, or o1
and o2
are direct subobjects of objects p1
and p2
, respectively, and p1
is transparently replaceable by p2
.o1
and o2
are complete objects.=> A
is transparently replaceable by A
, so any pointer that referred to the old A
will automatically point to the new A
after placement new, without requiring a std::launder
.
The compiler is allowed to assume that a truly const variable (not merely a const&, but an object variable declared const) will never change.
That is generally true, but only for complete objects that are const
and which are not allocated on the heap (have dynamic storage duration).
(All objects that are not subobjects of other objects are called complete objects)
This is covered by [basic.life] (10):
(10) Creating a new object within the storage that a const complete object with static, thread, or automatic storage duration occupies, or within the storage that such a const object used to occupy before its lifetime ended, results in undefined behavior.
Example:
struct A { const int i; };
// foo is a complete object, but not const. (storage can be reused)
// foo.i is const, but not a complete object. (storage can be reused)
A foo {1};
// bar is a const complete object. (storage cannot be reused)
// bar.i is const, but not a complete object.
// (but it is part of a const complete object => storage cannot be reused)
const A bar {1};
// ptrFoo points to a const complete object.
// (but its storage could be reused because it has dynamic storage duration)
A* ptrFoo = new const A{1};
A more interesting case would be if you're creating a new object in the storage of a
that is not transparently replaceable.
e.g.:
struct A {
int const n;
void f();
void g() { std::cout << this->n; }
void h() { std::cout << std::launder(this)->n; }
};
struct B : A {};
void A::f() {
new (this) B{2};
// using this here (calling a member method,
// accessing a data member, etc...) would be UB
}
static_assert(sizeof(B) == sizeof(A));
int main() {
auto a = A{1};
a.f();
std::cout << std::launder(&a)->n; // OK
a.h(); // UB
a.g(); // UB
std::cout << a.n; // UB
std::cout << std::launder(&a)->h(); // OK
new (&a) B{3};
std::cout << std::launder(&a)->n; // OK
a.h(); // UB
a.g(); // UB
std::cout << a.n; // UB
std::cout << std::launder(&a)->h(); // OK
}
Note that in this case calling a.h()
and a.g()
is already UB - you can't use a
at all to access the object (that includes invoking member methods on it) unless you launder it first.
This is covered by:
[basic.life] (7)
(7) Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction. Otherwise, such a glvalue refers to allocated storage, and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:
(7.1) the glvalue is used to access the object, or
(7.2) the glvalue is used to call a non-static member function of the object, or
(7.3) the glvalue is bound to a reference to a virtual base class, or
(7.4) the glvalue is used as the operand of a dynamic_cast or as the operand of typeid.
std::cout << a.n;
would be (7.1) because it accesses the object.a.h()
/ a.h()
would be (7.2) because it calls a member function on the object.