'Why does the use of `std::aligned_storage` allegedly cause UB due to it failing to "provide storage"?
Inspired by: Why is std::aligned_storage to be deprecated in C++23 and what to use instead?
The linked proposal P1413R3
(that deprecates std::aligned_storage
) says that:
Using
aligned_*
invokes undefined behavior (The types cannot provide storage.)
This refers to [intro.object]/3
:
If a complete object is created ([expr.new]) in storage associated with another object e of type “array of N
unsigned char
” or of type “array of Nstd::byte
” ([cstddef.syn]), that array provides storage for the created object if: ...
The standard then goes on to use the term "provides storage" in a few definitions, but I don't see it saying anywhere that using a different type as storage for placement-new (that fails to "provide storage") causes UB.
So, the question is: What makes std::aligned_storage
cause UB when used for placement-new?
Solution 1:[1]
The paper appears to be wrong on this.
If std::aligned_storage_t
failed to "provide storage", then most uses of it would indirectly cause UB (see below).
But whether std::aligned_storage_t
can actually "provide storage" appears to be unspecified. A common implementation that uses a struct with alignas(Y) unsigned char arr[X];
member (seemingly) does "provide storage" according to [intro.object]/3
, even if you pass the address of the whole structure into placement-new, rather than the array. Even though this specific implementation isn't mandated now, I believe mandating it would be a simple non-breaking change.
If std::aligned_storage_t
actually didn't "provide storage", then most use cases would cause UB:
Placement-new into an object that fails to "provide storage" is legal by itself, but...
This ends the lifetime of the object that failed to "provide storage" (aligned_storage_t
), and, recursively, all enclosing objects. The next time you access any of those, you get UB.
Even if aligned_storage_t
is not nested within other objects (which is rare), you'd have to be careful when destroying it, since calling its destructor would also cause UB, since its lifetime has already ended.
... The lifetime of an object o of type T ends when:
— the storage which the object occupies ... is reused by an object that is not nested within [the object]
An object a is nested within another object b if:
—a is a subobject of b, or
— b provides storage for a, or
— there exists an object c where a is nested within c, and c is nested within b.
Solution 2:[2]
The C++ standard lets a very restricted set of types serve as storage for other objects. The set of types that can serve as storage for other objects cannot themselves have alignment packaged into their type.
Imagine:
template<std::size_t N>
using bytes=std::byte[N];
template<std::size_t S, std::size_t A>
struct alignas(A) aligned{
bytes<S> data;
};
You cannot use &aligned<12,4>
to store another object safely. You cannot make a typedef that carries alignment with it with this property.
You could use aligned<12,4> a; &a.data
or similar, but that is syntactically different.
Now, the standard could get around it by adding wording; but the aligned storage existing definition does not have this magic wording, and no construct in C++ can have the properties users of aligned_storage_t
are expecting without such wording. I mean, UB is UB, so the compiler is free to interpret your program as if it was a program in a language with that wording... but that is swatting a standard error with a nuclear bomb.
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 | |
Solution 2 | Yakk - Adam Nevraumont |