Reusing data member storage via placement new during enclosing object's lifetime

佐手、 提交于 2019-12-18 18:09:18

问题


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.


回答1:


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

  • the destructor of T is trivial, or
  • for delete U is derived from T, or
  • you're using a destroying delete

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.




回答2:


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.]



来源:https://stackoverflow.com/questions/59239085/reusing-data-member-storage-via-placement-new-during-enclosing-objects-lifetime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!