3

This is a follow-up to my previous question where I seem to have made the problem more involved than I had originally intended. (See discussions in question and answer comments there.) This question is a slight modification of the original question removing the issue of special rules during construction/destruction of the enclosing object.


Is it allowed to reuse storage of a non-static data member during the lifetime of its enclosing object 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;

    void construct() {
        t.~T();
        u = ::new(static_cast<void*>(&t)) U /*initializer*/;
    }

    void destruct() {
        u->~U();
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

    A() = default;
    A(const A&) = delete;
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;
};

int main() {
    auto a = new A;
    a->construct();
    *(a->u) = /*some assignment*/;
    a->destruct(); /*optional*/
    delete a; /*optional*/

    A b; /*alternative*/
    b.construct(); /*alternative*/
    *(b.u) = /*some assignment*/; /*alternative*/
    b.destruct(); /*alternative*/
}

Aside from the static_asserts assume that the initializers, destructors and assignments of T and U do not throw.

What conditions do object types T and U need to satisfy additionally, 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 and the destruct function. Of course using it while its storage is occupied by a different type is not allowed.

Also note that the program constructs an object of the original type in t before its destructor is called in all execution paths since I disallowed T and U to throw exceptions.


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.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • You should probably indicate a specific C++ std version for these fine questions. – curiousguy Dec 12 '19 at 04:30
  • @curiousguy I have tagged C++20. I assume it is stable enough that an answer is unlikely to be invalidated before it is finalized. Otherwise I would have picked C++17. – walnut Dec 12 '19 at 05:10
  • I posted a [question about the change in the std regarding re-creation of objects](https://stackoverflow.com/q/59298904/963864). – curiousguy Dec 13 '19 at 05:49

2 Answers2

0

If a is destroyed (whether by delete or by falling out of scope), then t.~T() is called, which is UB if t isn't actually a T (by not calling destruct).

This doesn't apply if

After destruct is called you are not allowed to use t if T has const or reference members (until C++20).

Apart from that there is no restriction on what you do with the class as written as far as I can see.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • It seems to me that the restriction on using `t` after `destruct` would also apply to the implicit destruct call. Would a `const` member in `T` then make destroying the `A` UB? – walnut Dec 09 '19 at 14:37
0

This answer is based on the draft available at http://eel.is/c++draft/

We can try to apply (by checking each condition) what I've decided to call the "undead object" clause to any previous object that used to exist, here we apply it to the member t of type T:

Lifetime [basic.life]/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 original object is neither a complete object that is const-qualified nor a subobject of such an object, and

(8.4) neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).

Conditions 1 and 2 are automatically guaranteed by the use of placement new on the old member:

struct A {
    T t /*initializer*/; (...)

    void destruct() { (...)
        ::new(static_cast<void*>(&t)) T /*initializer*/;
    }

The location is the same and the type is the same. Both conditions are easily verified.

Neither A objects created:

auto a = new A;
...
A b; /*alternative*/

are const qualified complete objects so t isn't a member of a const qualified complete object. Condition 3 is met.

Now the definition of potentially-overlapping is in Object model [intro.object]/7:

A potentially-overlapping subobject is either:

(7.1) a base class subobject, or

(7.2) a non-static data member declared with the no_­unique_­address attribute.

The t member is neither and condition 4 is met.

All 4 conditions are met so the member name t can be used to name the new object.

[Note that at no point I even mentioned the fact the subobject isn't a const member not its subobjects. That isn't part of the latest draft.

It means that a const sub object can legally have its value changed, and a reference member can have its referent changed for an existing object. This is more than unsettling and probably not supported by many compilers. End note.]

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • 1
    I certainly considered the paragraph you are quoting, but I don't think it is that simple. For example I was made aware in another comment thread that [\[intro.object\]/2](https://eel.is/c++draft/basic#intro.object-2) actually makes the objects created by placement-new *not* subobjects. So then by the definition of *complete object*, they will actually be complete objects. Then there is also the question of whether reuse of part of the object is even allowed without ending the lifetime of the enclosing object, aside from nested types. I think these issues require additional considerations. – walnut Dec 11 '19 at 04:48
  • 1
    Then there is also the question of whether in the quoted paragraph the "*original object*" would be the initial `T` occupying that storage or the intermediate `U` when the new `T` is created? I assume the latter, because it says "*before the storage which the object occupied is reused*". – walnut Dec 11 '19 at 04:52
  • "_the objects created by placement-new not subobjects_" They are a complete object as anything created by `new` but then when all the 4 conditions are met, the name of a member refers to them so they are also *seen as a subobject*. I agree it's problematic on many level but the intent of being able to apply the clause to a (member or array element) subobject couldn't be more clear, the clause explicitly mention the old object potentially being a subobject. – curiousguy Dec 11 '19 at 04:56
  • @walnut "_the "original object" would be the initial T_" It's that one because we are concerned about that one; the clause doesn't speak of either one in particular, it can be either. It could be either of 100 objects if 100 objects have been created in the same storage location. **There is no expiry time for object un-dead-ism.** You can re-create any object that used to be there, at any point in time. You can also alternate between two objects of type T and U. – curiousguy Dec 11 '19 at 05:00
  • @walnut "_I certainly considered the paragraph you are quoting_" As a language lawyer oriented person, I suspected you were aware. But then this looked like the most relevant part of the std so I quoted it... – curiousguy Dec 11 '19 at 05:30