问题
Is it allowed to reuse storage of a non-static data member and if so under what conditions?
Consider the program
#include<new>
#include<type_traits>
using T = /*some type*/;
using U = /*some type*/;
static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));
struct A {
T t /*initializer*/;
U* u;
A() {
t.~T();
u = ::new(static_cast<void*>(&t)) U /*initializer*/;
}
~A() {
u->~U();
::new(static_cast<void*>(&t)) T /*initializer*/;
}
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};
int main() {
auto a = new A;
*(a->u) = /*some assignment*/;
delete a; /*optional*/
A b; /*alternative*/
*(b.u) = /*some assignment*/; /*alternative*/
}
What conditions do object types T
and U
need to satisfy in addition to the static_assert
s, so that the program has defined behavior, if any?
Does it depend on the destructor of A
actually being called (e.g. on whether the /*optional*/
or /*alternative*/
lines are present)?.
Does it depend on the storage duration of A
, e.g. whether /*alternative*/
lines in main
are used instead?
Note that the program does not use the t
member after the placement-new, except in the destructor. Of course using it while its storage is occupied by a different type is not allowed.
Please also note that I do not encourage anyone to write code like that. My intention is to understand details of the language better. In particular I did not find anything forbidding such placement-news as long as the destructor is not called, at least.
See also my other question regarding a modified version that does not execute the placement-news during construction/destruction of the enclosing object, since that seems to have caused complications according to some comments.
Concrete example as requested in comments demonstrating the wider question for a subset of types that I think represent different cases of interest:
#include<new>
#include<type_traits>
struct non_trivial {
~non_trivial() {};
};
template<typename T, bool>
struct S {
T t{};
S& operator=(const S&) { return *this; }
};
template<bool B>
using Q = S<int, B>; // alternatively S<const int, B> or S<non_trivial, B>
using T = Q<true>;
using U = Q<false>;
static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));
struct A {
T t;
U* u;
A() {
t.~T();
u = ::new(static_cast<void*>(&t)) U;
}
~A() {
u->~U();
::new(static_cast<void*>(&t)) T;
}
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};
int main() {
auto a = new A;
*(a->u) = {};
delete a; /*optional*/
// A b; /*alternative*/
// *(b.u) = {}; /*alternative*/
}
回答1:
That looks ok, with some problems depending on the contents of T
or U
, or if T::T
throws.
From cppreference
If a new object is created at the address that was occupied by another object, then all pointers, references, and the name of the original object will automatically refer to the new object and, once the lifetime of the new object begins, can be used to manipulate the new object, but only if the following conditions are satisfied:
- the storage for the new object exactly overlays the storage location which the original object occupied
- the new object is of the same type as the original object (ignoring the top-level cv-qualifiers)
- the type of the original object is not const-qualified
- if the original object had class type, it does not contain any non-static data member whose type is const-qualified or a reference type
- 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).
And you must GUARANTEE the new object is created, including in exceptions.
Directly from the standard:
[basic.life] 6.8/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:
- (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and
- (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
- (8.3) 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
- (8.4) 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).
That applies when T
is U
basically.
As for reusing space for T
with a different U
then backfilling:
[basic.life] 6.8/9:
(9) If a program ends the lifetime of an object of type T with static, thread, or automatic storage duration and if T has a non-trivial destructor,the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.
And T
(nor U
) cannot contain anything non-static const
.
[basic.life] 6.8/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.
来源:https://stackoverflow.com/questions/59237684/reusing-data-member-storage-via-placement-new