是新的,删除在 C++14 中仍然有用

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

本文关键字:有用 C++14 删除      更新时间:2023-10-16

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

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

如果需要编写任何需要就地构造的内容,例如:

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

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

对于要写入的某些容器,可能需要使用原始指针进行存储。

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

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

首先,非自定义放置new(我将忽略"非自定义"部分,仅称其为放置new)是与标准(非放置)new完全不同的纸牌游戏。 它在逻辑上与手动调用析构函数配对。 标准new既从免费存储中获取资源,又在其中构造对象。 它与 delete 配对,这会破坏对象并将存储回收到免费存储。 从某种意义上说,标准new内部调用放置new,标准delete在内部调用析构函数。

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

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

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

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

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

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

此外,make_shared会导致对象本身的资源分配(存储)持续存在,只要weak_ptr持续存在:在某些情况下,这可能不是可取的,因此您需要执行shared_ptr<T>(new T(...))以避免这种情况。

在少数情况下,如果要调用非放置new,可以调用 make_unique ,如果要与该unique_ptr分开管理,则可以.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

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

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

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

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