Preface: This is a long answer. Sorry about that! The fundamental answer is short and sweet. But there are a lot of bad arguments out there so there are a lot of basics to touch on.
The proposal claims "Using aligned_* invokes undefined behavior (The types cannot provide storage.)" but provides no additional discussion, proof, or references to language in the standard.
This appears to be a "language-lawyering" position based on an overly literal interpretation of "provides storage" as used in 6.7.2 of the C++ standard. The claim appears to draw from section 6.7.2.3 which states:
If a complete object is created (7.6.2.7) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std::byte” (17.2.1), that array provides storage for the created object ...
The apparent argument then goes on to say, to paraphrase, "Well, aligned_storage_t isn't an array of bytes. It is a struct/union/class that contains an array of bytes. So while that array could provide storage, aligned_storage_t itself cannot." The obvious issue with this argument is... if section 6.7.2.3 doesn't apply here, some other section in the standard still might. There are plenty of other "things" in the standard that "provide storage".
But all of that is irrelevant. What isn't in debate is that the unsigned char array contained within aligned_storage_t can provide storage. We can access that storage if we can get a pointer to it (e.g., for placement new or memcpy of a trivially-copyable type).
And per the standard in the clearest language, we can get a unsigned char* (which points to the beginning of array storage) through the pointer to aligned_storage_t. This is defined behavior to get a pointer to the 'storage' contained inside aligned_storage_t:
std::aligned_storage_t<sizeof(T), alignof(T)> buf;
unsigned char* storage = reinterpret_cast<unsigned char*>(&buf);
T* tptr = new(storage) T;
tptr->~T();
Why? Because aligned_storage_t is implemented either as a union with the unsigned char array as a non-static data member or as standard-layout class (e.g., POD struct) with the array as the first non-static data member.
See section 6.8.2 Compound types of the standard on pages 72-73:
4 Two objects a and b are pointer-interconvertible if:
(4.2) — one is a union object and the other is a non-static data member of that object (11.5), ...
(4.3) — one is a standard-layout class object and the other is the first non-static data member of that object, ...
And the term "pointer-interconvertible" means:
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a
pointer to one from a pointer to the other via a reinterpret_cast (7.6.1.9). [Note: An array object and its
first element are not pointer-interconvertible, even though they have the same address. — end note]
And that's it. The array contained in aligned_storage_t does provide storage and we can legally get a pointer to that storage through the "pointer-interconvertible" rules.
EDIT: To address the discussion with Language Lawyer in the comments, there is in fact a literal "pointer to an array" (fun fact). Semantically, it has one more level of indirection (in a sense, a pointer to a pointer) than an array type. But logically, the pointer to an object of type array N T
-> points to the beginning of array storage -> points to the location of the first element in the array. So:
char buf[4];
char (*ptr_buf)[4] = &buf;
char* ptr_elem0 = &buf[0];
ptr_buf
and ptr_elem
have different types but the same address. They are not interconvertible. See the accepted answer over here. This language in the standard forbids something like this:
struct MyStruct {
char name[4];
int value;
};
void g(char *chr) {
char (*name)[4] = reinterpret_cast<char (*)[4]>(chr); // Invalid
MyStruct* s = reinterpret_cast<MyStruct *>(name);
// This function uses a pointer to the first element in the array to
// access another member of the containing struct. C++ forbids this.
s->value = 10;
}
void f() {
MyStruct s;
g(&s.name[0]);
}
But that language and restriction is irrelevant here. These pointer-interconvertible "rules" are relevant to get access to first non-static element within aligned_storage_t
(the array that provides storage). It doesn't matter that we can't interconvert from unsigned char*
back to unsigned (*) [sizeof(T)]
(because we aren't even attempting to do that).
An array is convertible to a pointer to the first element (per Section 7.3.2). And that pointer to the first element can be used to access the entire array (because pointer arithmetic has defined behavior and arrays are contiguous). That pointer is all we need to provide to placement new. Note that operator new takes a void*
so array-to-pointer conversion will happen anyways. It makes no difference if we do that array-to-pointer conversion ourselves beforehand.
~aligned_storage_t()
on such an object causes UB becausealigned_storage_t
is dead at that point. – Vincentiaalignas
seems the best option, assuming that core language feature meets with any approval by the committee, and by the compiler vendors (who have representation on the committee). (Step 1: go to moon. Step 2: get rock. How hard could it be?) – Battista