C++14中的new和delete仍然有用吗?

62

除了支持旧代码外,在C++14中何时使用newdelete?鉴于现在可以使用make_uniquemake_shared,以及由unique_ptrshared_ptr析构函数自动删除内存的能力。


6
你的问题实际上也适用于C++11(如果包括Boost库则更早)。 - Cory Kramer
我不认为这是那个问题的重复。即使您从未使用new和/或delete,仍然需要使用原始指针。 - Galik
4
我提到 C++14 只是因为 C++11 标准中只有 make_shared 而没有 make_unique,这个遗漏使得 new 有了用处。 - Michael
2
不值得单独回答,因此任何答案都可以复制这个 - 我相信“new”仍然是执行原地构造的惯用方式。 - Drew Dormann
为什么要让事情变得更加复杂,当对于科学或算法编程来说,new和delete是更明智的选择呢? - FreelanceConsultant
显示剩余2条评论
4个回答

39
虽然在许多情况下智能指针比原始指针更可取,但在C++14中仍有许多使用new/delete的用例。如果需要编写任何需要就地构建的内容,例如:内存池、分配器、标记变量、二进制消息到缓冲区,那么您将需要使用放置new,可能还需要使用delete。对于某些要编写的容器,您可能希望使用原始指针进行存储。即使对于标准智能指针,如果要使用自定义删除器,您仍将需要new,因为make_uniquemake_shared不允许这样做。

11
如果你正在使用一个已经实现了垃圾回收机制的预设库(比如Qt),那么如果需要的话,你也许需要加上new关键字。 - sbabbi
1
有趣。但是,放置 new 是一个独立的问题。假设 C++2x 现在添加了 place_unique 和 place_shared,那么还需要 new/delete 吗? - Michael
1
对于原地构建,有 std::allocator 可以干净而通用地 allocate/deallocateconstruct/destroy 元素。 - edmz
1
@Michael 世界上什么是place_shared?放置new是一种语法,可以直接在内存块上调用构造函数。非放置new首先是获取空间,然后进行构造。unique_ptrshared_ptr是关于管理生命周期的。make_uniquemake_shared是在智能指针中获取资源、构造和管理它们的组合。由于放置new不涉及资源(只涉及构造),因此与资源管理正交。 - Yakk - Adam Nevraumont
3
@NirFriedman 是的:非定制放置 new 和非放置 new 一样,需要手动调用析构函数,以配成一对。从某种意义上说,放置 new 只是与非放置 new 略微相关:非放置 new 实际上调用了 new,而 delete 调用了析构函数。 - Yakk - Adam Nevraumont
显示剩余6条评论

7
使用make_uniquemake_shared相对于直接调用new来说是一种比较常见的选择,但这并不是强制性的。假设你选择遵循这个约定,那么有几个地方需要使用new
首先,非自定义的定位new(我将忽略“非自定义”的部分,只称之为定位new)与标准(非定位)new是完全不同的。它逻辑上与手动调用析构函数配对。标准new既从空闲存储中获取资源,又在其中构造一个对象。它与delete配对,后者销毁对象并将存储回收到空闲存储区。在某种意义上,标准new在内部调用定位new,而标准delete在内部调用析构函数。
定位new是一种在某些存储空间上直接调用构造函数的方法,它是高级生命周期管理代码所必需的。如果你正在实现optional、一个类型安全的union或一个智能指针(具有统一存储和非统一生命周期,如make_shared),你将使用定位new。然后在特定对象的生命周期结束时,直接调用它的析构函数。与非定位newdelete一样,定位new和手动析构函数调用是成对出现的。
自定义定位new是使用new的另一个原因。自定义定位new可用于从非全局池(作用域分配或分配到跨进程共享内存页、分配到视频卡共享内存等)中分配资源和其他目的。如果你想编写make_unique_from_custom,以使用自定义定位new分配内存,那么你必须使用new关键字。自定义定位new可以像定位new一样操作(即实际上不会获取资源,而是某种方式传递资源),也可以像标准new一样操作(即获取资源,可能使用传递的参数)。
如果自定义定位new抛出异常,则会调用自定义定位delete,因此你可能需要编写它。在C++中,不是你调用自定义定位delete,而是它调用你的重载函数。
最后,make_sharedmake_unique是不完整的函数,因为它们不支持自定义删除器。
如果你正在编写 make_unique_with_deleter,你仍然可以使用 make_unique 来分配数据,并将其 .release() 到你的 unique-with-deleter 中进行处理。如果你的删除器想要将其状态存储到指向的缓冲区中而不是存储到 unique_ptr 或单独分配的内存中,则需要在此处使用定位 new
对于 make_shared,客户端代码无法访问“引用计数桩”创建代码。据我所知,您不能轻松地同时拥有“对象和引用计数块的组合分配”和自定义删除器。
此外,make_shared 导致对象本身的资源分配(存储)持续存在,只要 weak_ptr 持续存在:在某些情况下,这可能不是理想的,因此您需要执行 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 让无状态的删除器不使用 up 空间,但是可能我搞砸了。
在一个库级别的解决方案中,如果 T::T(Ts...)noexcept 的话,我可以去掉 bCreated 的开销,因为在构造 T 之前不需要销毁 custom_delete

HmmпјҢcppreferenceеЈ°з§°allocate_sharedеҮҪж•°дёӯзҡ„вҖңеҲҶй…ҚеҷЁзҡ„еүҜжң¬иў«еӯҳеӮЁеңЁжҺ§еҲ¶еқ—дёӯпјҢд»ҘдҫҝеңЁе…ұдә«еј•з”Ёи®Ўж•°е’Ңејұеј•з”Ёи®Ўж•°йғҪиҫҫеҲ°йӣ¶ж—¶еҸҜд»ҘдҪҝз”Ёе®ғжқҘйҮҠж”ҫеҶ…еӯҳвҖқпјҢдҪҶжҲ‘еңЁж ҮеҮҶдёӯжүҫдёҚеҲ°иҝҷж ·зҡ„дҝқиҜҒгҖӮ - dyp
是的,我并没有评论那段话的第二部分(控制块和拥有对象的单独删除),而是关注于第一部分(通过单个分配创建具有自定义删除器的shared_ptr)。 - dyp
标准中的措辞对我来说看起来有点缺陷:它不要求存储分配器并使用它来析构对象,但至少libc++会这样做。仅使用分配器的分配部分似乎也有点愚蠢。 - dyp
我仍然不明白如何使用allocate shared得到一个启用自定义deleter的shared_ptr。在shared_ptr上下文中,deleter指的是非常具体的东西吗?无论如何,我想我已经找出如何创建带有任意状态的自定义deleter单缓冲区shared ptr,并将其添加到我的上面的回答中。问题的关键在于,我将原始缓冲区指针强制转换为void指针并在销毁时转换为T指针:虽然我认为这是有效的(因为T是在那里构造的),但标准中的规则并不是我所理解的。我不想存储“r”。 - Yakk - Adam Nevraumont
分配器提供了constructdestroy函数。对于某些分配器,调用construct而不是placement-new(例如scoped_allocator_adapter)非常重要。虽然我不知道任何关于destruct的真实用例。我希望支持分配器shared_ptr在构建和销毁时都使用分配器接口。但标准在这里非常模糊,可能存在缺陷。我知道在实现方面存在与std::vector和此接口的使用相关的问题,就我所看到的而言,在libc++中也存在类似的问题。 - dyp
显示剩余4条评论

4
我能想到的唯一理由是,偶尔你可能希望在你的unique_ptrshared_ptr中使用自定义删除器。为了使用自定义删除器,你需要直接创建智能指针,并传入new的结果。即使这种情况并不经常出现,但实际上确实会有这种情况出现。
除此之外,make_shared/make_unique应该涵盖几乎所有用途。

1
我的回答中的设计是否存在错误,允许您创建带有自定义删除器的shared_ptr而不直接调用非放置new?对于unique_ptr,甚至更容易:创建std::unique_ptr<T, D>(make_unique<T>(...).release(), deleter)--没有调用new!(如果deleter构造函数抛出,则无效)。 - Yakk - Adam Nevraumont

1
我认为仅有使用智能指针时才需要使用newdelete
例如,该库仍未拥有像boost::intrusive_ptr这样的内部指针,这是遗憾的,因为根据Andrei Alexandrescu的观点,它们在性能方面优于共享指针。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接