通过放置新的方式重复使用数据成员存储 [英] Reusing data member storage via placement new

查看:79
本文介绍了通过放置新的方式重复使用数据成员存储的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

是否允许重用非静态数据成员的存储,如果可以,在什么条件下可以重用?

考虑程序

#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*/
}

除了static_assert之外,对象类型TU还需要满足哪些条件,以便程序定义行为(如果有的话)?

它是否取决于实际调用的A的析构函数(例如,是否存在/*optional*//*alternative*/行)?

它是否取决于A的存储持续时间,例如是否使用main中的/*alternative*/行?


请注意,除析构函数外,程序在新放置后不使用t成员.当然,不允许在存储空间被其他类型占用的情况下使用它.


也请注意,我不鼓励任何人编写这样的代码.我的目的是更好地理解语言的细节.特别是,至少在不调用析构函数的情况下,我没有发现任何禁止此类新闻的消息.


另请参阅我的其他问题关于在构造/销毁封闭对象期间不执行放置新闻的修改版本,因为根据某些评论,这似乎引起了复杂性.


评论中要求的具体示例,针对我认为代表不同兴趣的类型的子集展示了更广泛的问题:

#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*/
}

解决方案

这看起来还可以,根据TU的内容或T::T抛出的情况,会出现一些问题.

来自 cppreference

如果在另一个对象占用的地址处创建了一个新对象,则所有指针,引用和原始对象的名称将自动引用该新对象,并且一旦新对象的生命周期开始,可用于操作新对象,但前提是必须满足以下条件:

  • 新对象的存储空间正好覆盖了原始对象所占据的存储空间
  • 新对象与原始对象具有相同的类型(忽略顶级简历限定符)
  • 原始对象的类型不是const限定的
  • 如果原始对象具有类类型,则它不包含任何类型为const限定或引用类型的非静态数据成员
  • 原始对象是类型T的最大派生对象,而新对象是类型T的最大派生对象(也就是说,它们不是基类的子对象).

并且您必须保证新对象的创建,包括例外情况.

直接来自标准:

[basic.life] 6.8/8 :

(8)如果在对象的生存期结束之后并且在重新使用或释放​​该对象所占用的存储之前,在原始对象所占用的存储位置上创建了一个新对象,则该指针指向该对象.原始对象,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦开始新对象的生命周期,就可以使用它来操作新对象,如果: /p>

  • (8.1)新对象的存储空间正好覆盖了原始对象所占据的存储位置,并且
  • (8.2)新对象与原始对象具有相同的类型(忽略顶级cv限定词),并且
  • (8.3)原始对象的类型不是const限定的,并且,如果是类类型,则不包含任何类型为const限定的非静态数据成员或引用类型,并且
  • (8.4)原始对象是类型T的最大派生对象,而新对象是类型T的最大派生对象(也就是说,它们不是基类的子对象).

T基本上是U时适用.

关于使用不同的U重用T的空间,然后回填:

[basic.life] 6.8/9 :

(9)如果程序以静态,线程或自动存储期限结束T类型的对象的生存期,并且T具有非平凡的析构函数,则程序必须确保原始类型的对象占用了以下内容:隐式析构函数调用发生时的相同存储位置;否则,该程序的行为是不确定的.即使该块异常退出也是如此.

T(也不是U)不能包含任何非静态const.

[basic.life] 6.8/10 :

(10)在具有静态,线程或自动存储持续时间的const完整对象所占用的存储中,或在此类const对象在其生命周期结束前曾经占用的存储中创建新对象,将导致未定义的行为

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_asserts, 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*/
}

解决方案

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.

这篇关于通过放置新的方式重复使用数据成员存储的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆