new 和 delete 在 C++14 中还有用吗?
Are new and delete still useful in C++14?
考虑到 make_unique
和 make_shared
的可用性,以及 unique_ptr
和 shared_ptr
析构函数的自动删除,情况是什么(除了支持遗留代码)在 C++14 中使用 new
和 delete
?
我能想到的唯一原因是,有时您可能希望在 unique_ptr
或 shared_ptr
中使用自定义删除器。要使用自定义删除器,您需要直接创建智能指针,并传入 new
的结果。即使这并不常见,但它确实会在实践中出现。
除此之外,make_shared
/make_unique
似乎应该涵盖几乎所有用途。
我会说 new
和 delete
的唯一原因是要实现其他类型的智能指针。
例如,库仍然没有像 boost::intrusive_ptr 那样的侵入式指针,这很遗憾,因为正如 Andrei Alexandrescu 指出的那样,它们出于性能原因优于共享指针。
虽然在许多情况下智能指针优于原始指针,但在 C++14 中仍然有很多 new
/delete
的用例。
如果你需要编写任何需要就地构建的东西,例如:
- 内存池
- 一个分配器
- 一个标记的变体
- 到缓冲区的二进制消息
您将需要使用展示位置 new
,并且可能 delete
。没办法。
对于一些你想写的容器,你可能想使用裸指针来存储。
即使 对于标准智能指针,如果您想使用自定义删除器,您仍然需要 new
,因为 make_unique
和 make_shared
不允许这样。
使用 make_unique
和 make_shared
而不是对 new
的原始调用是一个相对常见的选择。但是,这不是强制性的。假设您选择遵循该约定,有几个地方可以使用 new
.
首先,非自定义放置 new
(我将忽略 "non-custom" 部分,只称它为放置 new
)是一种与标准(非-放置)new
。它在逻辑上与手动调用析构函数配对。标准 new
既从免费商店获取资源,又在其中构造一个对象。它与delete
配对,销毁对象并将存储回收到空闲存储。从某种意义上说,标准new
在内部调用了放置new
,而标准delete
在内部调用了析构函数。
Placement new
是您在某些存储上直接调用构造函数的方式,并且是高级生命周期管理代码所必需的。如果您正在实现 optional
、对齐存储上的类型安全 union
或智能指针(具有统一存储和非统一生命周期,如 make_shared
),您将使用放置 new
。然后在特定对象的生命周期结束时,您直接调用它的析构函数。与非放置 new
和 delete
一样,放置 new
和手动析构函数调用成对出现。
自定义展示位置 new
是使用 new
的另一个原因。自定义放置 new
可用于从非全局池分配资源——作用域分配,或分配到跨进程共享内存页面,分配到视频卡共享内存等——以及其他目的。如果你想编写 make_unique_from_custom
来使用自定义放置 new 分配其内存,则必须使用 new
关键字。自定义放置 new
可以像新放置一样(因为它实际上并不获取资源,而是以某种方式传入资源),或者它可以像标准 new
一样(因为它获取资源,也许使用传入的参数)。
如果自定义展示位置 new
抛出异常,则会调用自定义展示位置 delete
,因此您可能需要编写它。在 C++ 中你不调用自定义放置 delete
,它 (C++) 调用你(r overload).
最后,make_shared
和 make_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_ptr
s 持续存在时持续存在:在某些情况下,这可能是不可取的,因此你想做一个 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
之前销毁。
考虑到 make_unique
和 make_shared
的可用性,以及 unique_ptr
和 shared_ptr
析构函数的自动删除,情况是什么(除了支持遗留代码)在 C++14 中使用 new
和 delete
?
我能想到的唯一原因是,有时您可能希望在 unique_ptr
或 shared_ptr
中使用自定义删除器。要使用自定义删除器,您需要直接创建智能指针,并传入 new
的结果。即使这并不常见,但它确实会在实践中出现。
除此之外,make_shared
/make_unique
似乎应该涵盖几乎所有用途。
我会说 new
和 delete
的唯一原因是要实现其他类型的智能指针。
例如,库仍然没有像 boost::intrusive_ptr 那样的侵入式指针,这很遗憾,因为正如 Andrei Alexandrescu 指出的那样,它们出于性能原因优于共享指针。
虽然在许多情况下智能指针优于原始指针,但在 C++14 中仍然有很多 new
/delete
的用例。
如果你需要编写任何需要就地构建的东西,例如:
- 内存池
- 一个分配器
- 一个标记的变体
- 到缓冲区的二进制消息
您将需要使用展示位置 new
,并且可能 delete
。没办法。
对于一些你想写的容器,你可能想使用裸指针来存储。
即使 对于标准智能指针,如果您想使用自定义删除器,您仍然需要 new
,因为 make_unique
和 make_shared
不允许这样。
使用 make_unique
和 make_shared
而不是对 new
的原始调用是一个相对常见的选择。但是,这不是强制性的。假设您选择遵循该约定,有几个地方可以使用 new
.
首先,非自定义放置 new
(我将忽略 "non-custom" 部分,只称它为放置 new
)是一种与标准(非-放置)new
。它在逻辑上与手动调用析构函数配对。标准 new
既从免费商店获取资源,又在其中构造一个对象。它与delete
配对,销毁对象并将存储回收到空闲存储。从某种意义上说,标准new
在内部调用了放置new
,而标准delete
在内部调用了析构函数。
Placement new
是您在某些存储上直接调用构造函数的方式,并且是高级生命周期管理代码所必需的。如果您正在实现 optional
、对齐存储上的类型安全 union
或智能指针(具有统一存储和非统一生命周期,如 make_shared
),您将使用放置 new
。然后在特定对象的生命周期结束时,您直接调用它的析构函数。与非放置 new
和 delete
一样,放置 new
和手动析构函数调用成对出现。
自定义展示位置 new
是使用 new
的另一个原因。自定义放置 new
可用于从非全局池分配资源——作用域分配,或分配到跨进程共享内存页面,分配到视频卡共享内存等——以及其他目的。如果你想编写 make_unique_from_custom
来使用自定义放置 new 分配其内存,则必须使用 new
关键字。自定义放置 new
可以像新放置一样(因为它实际上并不获取资源,而是以某种方式传入资源),或者它可以像标准 new
一样(因为它获取资源,也许使用传入的参数)。
如果自定义展示位置 new
抛出异常,则会调用自定义展示位置 delete
,因此您可能需要编写它。在 C++ 中你不调用自定义放置 delete
,它 (C++) 调用你(r overload).
最后,make_shared
和 make_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_ptr
s 持续存在时持续存在:在某些情况下,这可能是不可取的,因此你想做一个 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
之前销毁。