我正在阅读"有效的现代C ++"。在与std::unique_ptr
相关的项目中,它声明如果自定义删除器是无状态对象,则不会出现大小费用,但如果它是函数指针或std::function
大小费用发生。你能解释一下原因吗?
我们说我们有以下代码:
auto deleter_ = [](int *p) { doSth(p); delete p; };
std::unique_ptr<int, decltype(deleter_)> up(new int, deleter_);
据我了解,unique_ptr
应该有decltype(deleter_)
类型的对象,并将deleter_
分配给该内部对象。但显然不是发生了什么。你能用最小的代码示例来解释这背后的机制吗?
答案 0 :(得分:35)
unique_ptr
必须始终存储其删除者。现在,如果删除器是没有状态的类类型,那么template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TYPE_VIS_ONLY unique_ptr
{
public:
typedef _Tp element_type;
typedef _Dp deleter_type;
typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
private:
__compressed_pair<pointer, deleter_type> __ptr_;
可以使用instantiates,这样删除器就不会使用任何额外的空间。
如何完成此操作在实现之间有所不同。例如,empty base optimization和MSVC都将托管指针和删除器存储在libc++中,如果涉及的类型之一是空类,它会自动为您提供空基优化。
从上面的libc ++链接
std::tuple
tuple
中的 libstdc ++ compressed pair和一些Google搜索建议他们的unique_ptr
实施采用空基优化,但我找不到明确说明的任何文档。
在任何情况下,stores the two都表明libc ++和libstdc ++都使用EBO来减少带有空删除符的{{1}}的大小。
答案 1 :(得分:19)
如果删除器是无状态的,则不需要存储空间。如果删除者不是无状态,那么状态需要存储在unique_ptr
本身
std::function
和函数指针具有仅在运行时可用的信息,因此必须与对象本身的指针一起存储在对象中。这又需要分配(在unique_ptr
本身)空间来存储该额外状态。
或许理解Empty Base Optimization将有助于您了解如何在实践中实施
std::is_empty
类型特征是如何实现这一特性的另一种可能性。
库编写者如何实现这一点显然取决于他们以及标准允许的内容。
答案 2 :(得分:13)
来自unique_ptr
实施:
template<class _ElementT, class _DeleterT = std::default_delete<_ElementT>>
class unique_ptr
{
public:
// public interface...
private:
// using empty base class optimization to save space
// making unique_ptr with default_delete the same size as pointer
class _UniquePtrImpl : private deleter_type
{
public:
constexpr _UniquePtrImpl() noexcept = default;
// some other constructors...
deleter_type& _Deleter() noexcept
{ return *this; }
const deleter_type& _Deleter() const noexcept
{ return *this; }
pointer& _Ptr() noexcept
{ return _MyPtr; }
const pointer _Ptr() const noexcept
{ return _MyPtr; }
private:
pointer _MyPtr;
};
_UniquePtrImpl _MyImpl;
};
_UniquePtrImpl
类包含指针,并派生自deleter_type
。
如果删除器恰好是无状态的,则可以优化基类,使其自身不占用任何字节。然后整个unique_ptr
可以与包含指针的大小相同 - 即:与普通指针大小相同。
答案 3 :(得分:5)
实际上将对不是无状态的lambdas进行大小惩罚,即捕获一个或多个值的lambda。
但是对于非捕获的lambdas,有两个关键事实需要注意:
因此,编译器能够纯粹基于其类型调用lambda,它被记录为unique_ptr
类型的一部分;不需要额外的运行时信息。
这实际上是为什么非捕获的lambdas是无国籍的。就大小惩罚问题而言,与任何其他无状态删除函子类型相比,非捕获lambdas当然没有什么特别之处。
请注意,std::function
不无状态,这就是为什么不适用于它的原因。
最后,请注意,尽管无状态对象通常需要具有非零大小以确保它们具有唯一地址,但无状态基类不需要添加到派生类型的总大小;这称为空基优化。因此unique_ptr
可以实现(如在Bo Perrson的答案中)作为从删除类型派生的类型,如果它是无状态的,则不会造成大小惩罚。 (事实上,这可能只是 方式正确实现unique_ptr
而没有无状态删除器的大小惩罚,但我不确定。)