new 和 delete 在 C++14 中还有用吗?

Are new and delete still useful in C++14?

考虑到 make_uniquemake_shared 的可用性,以及 unique_ptrshared_ptr 析构函数的自动删除,情况是什么(除了支持遗留代码)在 C++14 中使用 newdelete

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

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

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

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

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

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

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

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

对于一些你想写的容器,你可能想使用裸指针来存储。

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

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

首先,非自定义放置 new(我将忽略 "non-custom" 部分,只称它为放置 new)是一种与标准(非-放置)new。它在逻辑上与手动调用析构函数配对。标准 new 既从免费商店获取资源,又在其中构造一个对象。它与delete配对,销毁对象并将存储回收到空闲存储。从某种意义上说,标准new在内部调用了放置new,而标准delete在内部调用了析构函数。

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

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

如果自定义展示位置 new 抛出异常,则会调用自定义展示位置 delete,因此您可能需要编写它。在 C++ 中你不调用自定义放置 delete,它 (C++) 调用你(r overload).

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

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

对于 make_shared,客户端代码无权访问 "reference counting stub" 创建代码。据我所知,您不能同时拥有 "combined allocation of object and reference counting block" 和自定义删除器。

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

在少数情况下你想调用非放置new,你可以调用make_unique,然后.release()指针如果你想单独管理unique_ptr。这增加了您的 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 来允许无状态删除器使用 no up space,但我可能搞砸了。

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