Gathering from the hints throughout the countless helpful comments, here is my interpretation of what's happening.
TLDR its well-formed‡see edit
Quoting in the order I find more logical from [basic.life]†
The properties ascribed to objects and references throughout this International Standard apply for a given object or reference only during its lifetime.
An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. [...] The lifetime of an object of type T
begins when:
storage with the proper alignment and size for type T
is obtained, and
if the object has non-vacuous initialization, its initialization is complete.
The lifetime of an object o of type T
ends when:
if T is a class type with a non-trivial destructor , the destructor call starts, or
the storage which the object occupies is released, or is reused by an object that is not nested within o
From [basic.lval]†
If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined
the dynamic type of the object,
a cv-qualified version of the dynamic type of the object,
a type similar to the dynamic type of the object,
a type that is the signed or unsigned type corresponding to the dynamic type of the object,
a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
a char
, unsigned char
, or std::byte
type.
We deduce that
The lifetime of the char
s in the char[]
ends when another object reuses that space.
The lifetime of an object of type T
started when push_back
is called.
Since the address ((T*)data.get() + --sz)
is always that of an object with type T
whose lifetime has started and not yet ended, it is valid to call ~T()
with it.
During this process, the char[]
and char*
in aligned_memory
aliases objects of type T
but it is legal to do so. Also, no glvalue is obtained from them, so they could have been pointers of any type.
To answer my own question in the comments whether using any memory as storage is also well-formed
U u;
u->~U();
new (&u) T;
((T*)&u)->~T();
new (&u) U;
Following the 4 points above, the answer is yes‡see edit, as long as the alignment of U
is not weaker than T
.
‡ EDIT: I've neglected another paragraph of [basic.life]
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
the original object was a most derived object of type T
and the new object is a most derived object of type T
(that is, they are not base class subobjects).
Which means even though using the object is well-formed, the means which the object is obtained is not. Specifically, post C++17, std::launder
has to be called
(std::launder((T*)data.get()) + --sz)->~T();
Prior C++17, a workaround would be to use the pointer acquired from the placement new instead
T* p = new (data.get() + sz++ * sizeof(T)) T(t); // store p somewhere
† Quoted from n4659, as far as I can see, same holds for n1905