Consider this union:
union A{
int a;
struct{
int b;
} c;
};
c
and a
are not layout-compatibles types so it is not possible to read the value of b
through a
:
A x;
x.c.b=10;
x.a+x.a; //undefined behaviour (UB)
For Trial 1 and Trial 2 see this question
Now let's use std::launder
for what it does not seems to be intended:
A x;
x.a=10;
auto p = &x.a; //(1)
x.c.b=12; //(2)
p = std::launder(p); //(2')
*p+*p; //(3) UB?
Could std::launder
change anything? According to [ptr.launder]:
template <class T> constexpr T* launder(T* p) noexcept;
Requires:
p
represents the address A of a byte in memory. An object X that is within its lifetime and whose type is similar toT
is located at the address A. All bytes of storage that would be reachable through the result are reachable throughp
(see below).Returns: A value of type
T *
that points to X.Remarks: An invocation of this function may be used in a core constant expression whenever the value of its argument may be used in a core constant expression. A byte of storage is reachable through a pointer value that points to an object Y if it is within the storage occupied by Y, an object that is pointer-interconvertible with Y, or the immediately-enclosing array object if Y is an array element. The program is ill-formed if T is a function type or cv void.
The bolded sentence emphasizes a someting that troubles me. If p
is an invalid pointer value, how could any byte of storage be accessible? On the other hand with such a reading std::launder
is just not usable.
Otherwise, could p
's value at (2) be a pointer value that represents a region of storage as is talked about in a "Note" in [basic.life]:
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
([support.dynamic]).
It is explicitly allowed in a note.
basic.life
contains the following rule that makes std::launder
unnecessary:
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 storage for the new object exactly overlays the storage location which the original object occupied, and
- the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
- 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, and
- neither the original object nor the new object is a potentially-overlapping subobject.
[ 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
. — end note ]
This "new object is created at the storage location which the original object occupied" case clearly applies here because:
An object is created ... when implicitly changing the active member of a union...
All the bullet conditions are met, as "potentially overlapping subobject" refers to base class subobjects, which union members are not. (And in the version you linked to, that bullet mentioned base class subobjects directly.)
However even if this interpretation were to change against unions, the note specifically mentions that std::launder
bypasses this restriction.
Do note that older versions of the Standard excluded subobjects from this rule... but the note makes it clear that std::launder
would have bypassed that problem as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With