通过放置重新使用数据成员存储

时间:2019-12-08 16:57:36

标签: c++ language-lawyer placement-new

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

考虑程序

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

T之外,对象类型Ustatic_assert还需要满足什么条件,以便程序定义行为(如果有)?

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

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


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


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


另请参阅我的other question关于修改版本的说明,该修改版本在封装对象的构建/销毁过程中不执行放置新闻,因为根据某些评论,这似乎引起了复杂性。


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

#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 个答案:

答案 0 :(得分:-1)

这看起来还可以,根据TU的内容,或者如果T::T抛出,则会出现一些问题。

来自cppreference

  

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

     
      
  • 新对象的存储空间正好覆盖了原始对象所占据的存储空间
  •   
  • 新对象与原始对象的类型相同(忽略顶级cv限定词)
  •   
  • 原始对象的类型不是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时适用。

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

[basic.life] 6.8/9

  

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

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

[basic.life] 6.8/10

  

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