【问题标题】:Are new and delete still useful in C++14?new 和 delete 在 C++14 中仍然有用吗?
【发布时间】:2015-08-26 02:21:17
【问题描述】:

鉴于make_uniquemake_shared 的可用性,以及unique_ptrshared_ptr 析构函数的自动删除,在哪些情况下(除了支持遗留代码)使用newdelete C++14?

【问题讨论】:

  • 您的问题实际上也适用于 C++11(如果您包含 Boost,则更早)
  • 我看不出这是该问题的副本。即使您从不使用new 和/或delete,您仍然需要使用原始指针。
  • 我想说,当您想将特殊运算符 new 称为 no_throw、placement new 或实现自定义分配器(无论如何您都想包装在 make_my_unique 中)。
  • @CoryKramer:我提到 C++14 只是因为 C++11 标准只有 make_shared,但没有 make_unique,而这个省略使 new 有用。
  • 不值得一个单独的答案,所以任何答案都可以复制这个 - 我相信new仍然是执行in-place construction的惯用方式。

标签: c++ c++11 new-operator c++14 dynamic-memory-allocation


【解决方案1】:

虽然在许多情况下智能指针比原始指针更可取,但在 C++14 中仍有很多 new/delete 的用例。

如果你需要编写任何需要就地构造的东西,例如:

  • 内存池
  • 分配器
  • 标记的变体
  • 二进制消息到缓冲区

您将需要使用展示位置new,可能还需要使用delete。没办法。

对于某些您要编写的容器,您可能希望使用原始指针进行存储。

即使对于标准智能指针,如果你想使用自定义删除器,你仍然需要new,因为make_uniquemake_shared 不允许这样做。

【讨论】:

  • 如果您正在使用实现某种 GC 的预先存在的库(Qt,我在看你),您可能还需要 new
  • 有趣。不过,新的安置是一个单独的野兽。现在假设假设的 C++2x 将添加 place_unique 和 place_shared,是否还需要 new/delete?
  • 对于就地构造,std::allocator 允许干净且通用地使用 allocate/deallocateconstruct/destroy 元素。
  • @Michael place_shared 到底是什么?放置new 是直接在内存块上调用构造函数的语法。非放置new是先获取空间,再构造。 unique_ptrshared_ptr 是关于管理生命周期的。 make_uniquemake_shared 是在智能 ptr 中获取资源、构建和管理资源的组合。新布局,因为它不处理资源(只是构造),与资源管理正交。
  • @NirFriedman 是的:非自定义放置 new 与手动调用析构函数配对,就像非放置 new 与调用 delete 配对一样。从某种意义上说,放置new与非放置new只是切线相关:非放置new有效调用new,而delete调用析构函数。
【解决方案2】:

使用make_uniquemake_shared 而不是对new 的原始调用是一种相对常见的选择。然而,这不是强制性的。假设您选择遵循该约定,则有几个地方可以使用 new

首先,非自定义放置new(我将忽略“非自定义”部分,并称之为放置new)与标准(非放置)new完全不同的纸牌游戏.它在逻辑上与手动调用析构函数配对。标准new 都从免费存储中获取资源,并在其中构造一个对象。与delete配对,销毁对象并将存储回收到空闲存储。从某种意义上说,标准new在内部调用placementnew,标准delete在内部调用析构函数。

Placement new 是您在某些存储上直接调用构造函数的方式,并且是高级生命周期管理代码所必需的。如果您正在实现optional、对齐存储上的类型安全union,或智能指针(具有统一存储和非统一生命周期,如make_shared),您将使用放置new。然后在特定对象的生命周期结束时,直接调用它的析构函数。与非放置 newdelete 一样,放置 new 和手动析构函数调用成对出现。

自定义展示位置new 是使用new 的另一个原因。自定义放置new 可用于从非全局池中分配资源——范围分配,或分配到跨进程共享内存页面,分配到视频卡共享内存等——以及其他用途。如果您想编写make_unique_from_custom,使用自定义位置new 分配其内存,则必须使用new 关键字。自定义放置new 可以像placement new(因为它实际上并不获取资源,而是以某种方式传入资源),或者它可以像标准new 一样(因为它获取资源,可能使用传入的参数)。

如果自定义展示位置new 抛出,则调用自定义展示位置delete,因此您可能需要编写该代码。在 C++ 中,您不会调用自定义放置 delete,它 (C++) 会调用您(r 重载)

最后,make_sharedmake_unique 是不完整的函数,因为它们不支持自定义删除器。

如果您正在编写make_unique_with_deleter,您仍然可以使用make_unique 来分配数据,并使用.release() 将其交由您的unique-with-deleter 处理。如果您的删除器想要将其状态填充到指向缓冲区而不是 unique_ptr 或单独的分配中,则需要在此处使用放置 new

对于make_shared,客户端代码无权访问“引用计数存根”创建代码。据我所知,你不能轻易地同时拥有“对象和引用计数块的组合分配”和自定义删除器。

此外,make_shared 会导致对象本身的资源分配(存储)持续,只要weak_ptrs 持续存在:在某些情况下,这可能是不可取的,所以你想这样做shared_ptr<T>(new T(...)) 来避免这种情况。

在少数情况下,如果您想与unique_ptr分开管理,您可以调用make_unique,然后调用指针.release()。这增加了您的 RAII 资源覆盖率,并意味着如果有异常或其他逻辑错误,您不太可能泄漏。


我在上面提到过,我不知道如何使用自定义删除器和共享指针来轻松使用单个分配块。以下是如何巧妙地做到这一点的草图:

template<class T, class D>
struct custom_delete {
  std::tuple<
    std::aligned_storage< sizeof(T), alignof(T) >,
    D,
    bool
  > data;
  bool bCreated() const { return std::get<2>(data); }
  void markAsCreated() { std::get<2>()=true; }
  D&& d()&& { return std::get<1>(std::move(data)); }
  void* buff() { return &std::get<0>(data); }
  T* t() { return static_cast<T*>(static_cast<void*>(buff())); }
  template<class...Ts>
  explicit custom_delete(Ts...&&ts):data(
    {},D(std::forward<Ts>(ts)...),false
  ){}
  custom_delete(custom_delete&&)=default;
  ~custom_delete() {
    if (bCreated())
      std::move(*this).d()(t());
  }
};

template<class T, class D, class...Ts, class dD=std::decay_t<D>>
std::shared_ptr<T> make_shared_with_deleter(
  D&& d,
  Ts&&... ts
) {
  auto internal = std::make_shared<custom_delete<T, dD>>(std::forward<D>(d));
  if (!internal) return {};
  T* r = new(internal->data.buff()) T(std::forward<Ts>(ts...));
  internal->markAsCreated();
  return { internal, r };
}

我认为应该这样做。我试图通过使用tuple 来允许无状态删除器使用任何空间,但我可能搞砸了。

在库质量的解决方案中,如果 T::T(Ts...)noexcept,我可以删除 bCreated 开销,因为在 T 之前没有机会必须销毁 custom_delete构建。

【讨论】:

  • “据我所知,你不能同时拥有“对象和引用计数块的组合分配”和自定义删除器”我认为你可以使用allocate_shared为此目的。
  • @dyp 如何使用allocate_shared 注入自定义删除器?我知道如何存储额外的数据,但我不知道如何避免 T 的析构函数在引用计数变为零时被调用。我想我可以创建一个带有 bool 跟踪的缓冲区,如果它已经被构造和一个调用我的自定义删除操作的删除器,分配它,使用上帝模式共享 ptr 构造函数(shared_ptr&lt;T&gt;( shared_ptr&lt;Y&gt;&amp;, T* ) 别名来自指针的 shared_ptr 的类型到那个指向T的指针,同时构造和设置所说的bool?哎呀,有一个bool的开销!
  • Hmm cppreference 声称 allocate_shared “alloc 的副本存储为控制块的一部分,以便在共享和弱引用计数都达到零时可以使用它来释放它." 但我在标准中找不到这样的保证..
  • @dyp 无论如何,当弱指针计数变为零时会发生释放,而不是当强指针计数变为零时。当强指针计数达到零时调用删除器,这就是我试图注入单块共享 ptr 的内容。
  • 是的,我没有评论该段落的第二部分(单独删除控制块和拥有的对象),而是评论第一部分(通过单个自定义删除器创建 shared_ptr分配)。
【解决方案3】:

我能想到的唯一原因是,有时您可能希望在unique_ptrshared_ptr 中使用自定义删除器。要使用自定义删除器,您需要直接创建智能指针,并传入new 的结果。即使这并不常见,但在实践中确实会出现。

除此之外,make_shared/make_unique 似乎应该涵盖几乎所有用途。

【讨论】:

  • 我的回答中的设计是否有错误,它允许您使用自定义删除器创建shared_ptr,而无需直接调用非放置new?对于unique_ptr,它更容易:创建std::unique_ptr&lt;T, D&gt;( make_unique&lt;T&gt;(...).release(), deleter )——无需调用new! (如果deleter 构造函数抛出,则报价无效)。
【解决方案4】:

我想说newdelete 的唯一原因是实现其他类型的智能指针。

例如,库仍然没有像 boost::intrusive_ptr 这样的侵入式指针,这很遗憾,因为正如 Andrei Alexandrescu 指出的那样,由于性能原因它们优于共享指针。

【讨论】:

    猜你喜欢
    • 2020-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-26
    • 2023-03-22
    • 1970-01-01
    • 2011-05-27
    • 2017-12-07
    相关资源
    最近更新 更多