【问题标题】:Are only types with trivial destructor suited for storage for placement new?只有具有平凡析构函数的类型才适合存储新的放置吗?
【发布时间】:2018-07-02 08:24:04
【问题描述】:

放置 new 的示例通常使用 unsigned char 数组作为底层存储。步骤可以是:

  1. 用 new 创建 unsigned char 数组
  2. 在此存储中创建一个带有新位置的对象
  3. 使用对象
  4. 销毁对象
  5. 为 unsigned char 数组调用 delte 以释放数组

第 5 点似乎只有在我们使用带有微不足道的析构函数的底层存储类型时才有效。否则,我们将调用底层存储类型的析构函数,但其​​中不存在任何对象。从技术上讲,我们正在破坏一堆不存在的无符号字符,我们很幸运,无符号字符类型的析构函数是微不足道的,所以没有操作。

下面的代码呢:

struct A{ /* some members... */ };
struct B{ /* some members... B shall be same size as A */ };

int main()
{    
    auto ptr_to_a = new A; // A object lives @ ptr_to_a    
    ptr_to_a->~A(); // A object destroyed. no object living @ ptr_to_a, but storage is preserved
    new (ptr_to_a) B; // B object living @ ptr_to_a.    
    std::launder(reinterpret_cast<b*>(ptr_to_a))->/*...*/; // use B. for this purpose we need std::launder in C++17 or we would store the pointer returned by the placement new and use it without std::launder
    std::launder(reinterpret_cast<b*>(ptr_to_a))->~B(); // B object destroyed. no object living @ ptr_to_a, but storage is preserved

    // at this point there is no object living @ ptr_to_a, but we need to hand back the occupied storage.

    // a)
    delete ptr_to_a; // undefined behavior because no object is sitting @ ptr_to_a

    // b)
    new (ptr_to_a) A; // create an object again to make behavior defined. but this seems odd.
    delete ptr_to_a;

    // c)
    // some method to just free the memory somehow without invoking destructors?

    return 0;
}

https://en.cppreference.com/w/cpp/language/lifetime 上写着: 作为一种特殊情况,可以在 unsigned char 或 std::byte 数组中创建对象(在这种情况下,据说该数组为对象提供存储)如果...。

这是否意味着它只允许在 unsigned char 和 byte 数组上使用放置 new 并且因为它们有一个微不足道的析构函数,所以我的代码示例已经过时?

否则,我的代码示例怎么样?选项 b) 是唯一有效的解决方案吗?

编辑:第二个例子:

struct A{ /* some members... */ };
struct alignas(alignof(A)) B{ /* some members... */ };

int main()
{    
    static_assert(sizeof(A) == sizeof(B));
    A a;      
    a.~A();    
    auto b_ptr = new (&a) B; 
    b_ptr->~B();    
    return 0;
    // undefined behavior because a's destructor gets called but no A object is "alive" (assuming non trivial destructor)
    // to make it work, we need to placement new a new A into a?
}

【问题讨论】:

    标签: destructor c++17 placement-new


    【解决方案1】:

    通常,您不会使用分配不相关类 A 所返回的存储空间来放入您的 B。您甚至不必进行分配

    int main ()
    {
        char storage[sizeof(B)];
        std::aligned_storage<sizeof(B), alignof(B)>::type aligned_storage;
    
        auto b_ptr1 = new (&storage) B; // potentially unaligned
        auto b_ptr2 = new (&aligned_storage) B; // guaranteed safe
    
        // use b_ptr1, b_ptr2
    
        b_ptr1->~B();
        b_ptr2->~B();
        // storage ceases to exist when main returns
    }
    

    如果你确实需要动态分配,我建议将存储包装在一个持有者结构中,这样你就不会结束你 newed 的东西的生命周期。

    struct B_holder
    {
        std::aligned_storage<sizeof(B), alignof(B)>::type storage;
        B * make_B() { return new(&storage) B; }
    }
    
    int main()
    {
        auto holder = std::make_unique<B_holder>();
        auto * B_ptr = B_holder->make_B();
    
        // use B_ptr
    
        B_ptr->~B();
        // holder ceases to exist when main returns
    }
    

    【讨论】:

    • 这只是回避了我的问题。当然,我可以将我的 unsigned char 数组放在堆栈上。然后在退出作用域时调用析构函数。尽管如此,在这种情况下调用了析构函数,它是一个无操作析构函数,但必须如此吗?如果不是,我是否必须将原始类型的新对象放置到位以防止未定义的行为,或者只是简单地不允许重用具有非平凡析构函数的类的存储?
    • @phön 尽管它有效,但我建议使用任意其他类型进行存储只是一个坏主意。
    • 我同意这可能是个坏主意。我只是用它作为讨论的例子。所以我做了第二个例子,避免分配。当自动销毁开始时,我们需要一个有效的 A 对象,使其工作的唯一方法是放置新 A,对吗?对于具有琐碎析构函数的类型,我们不需要这样做吗?因为我们知道他们有一个无操作析构函数?从技术上讲,我们正在销毁不存在的东西(使用无符号字符时)
    【解决方案2】:

    大部分是的。

    但是你可以做这样的事情。

    struct placed {
      char stuff[100];
    };
    struct stupid {
      std::aligned_storage_t<sizeof(placed), alignof(placed)> data;
      ~stupid() {
        std::cout << "stupid gone\n";
      }
    };
    
    int main() {
      auto* pstupid = new stupid;
      auto* pplaced = ::new( (void*)&pstupid->data ) placed;
      pplaced->~placed();
      auto* pstupid2 = ::new( (void*)pstupid ) stupid;
      delete pstupid2;
    }
    

    但是,正如类型名称所暗示的那样,非常愚蠢。如果没有很多我上面没有包含的noexcept 保证,那么要让异常安全变得非常困难。

    我也不完全确定delete pstupid2 是否合法,因为它是通过placement new 而不是通过简单的new 表达式创建的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-04
      • 1970-01-01
      • 2015-09-02
      • 1970-01-01
      • 2015-08-10
      • 1970-01-01
      • 2015-08-26
      • 1970-01-01
      相关资源
      最近更新 更多