C++ std :: unique_ptr:为什么lambdas没有任何大小的费用?

jnb*_*mez 41 c++ unique-ptr c++11 c++14

我正在阅读"Effective Modern C++".在与std::unique_ptr它相关的项目中声明如果自定义删除器是无状态对象,则不会出现大小费用,但如果它是函数指针或std::function大小费用发生.你能解释一下原因吗?

假设我们有以下代码:

auto deleter_ = [](int *p) { doSth(p); delete p; };
std::unique_ptr<int, decltype(deleter_)> up(new int, deleter_);
Run Code Online (Sandbox Code Playgroud)

根据我的理解,unique_ptr应该有一个类型的对象decltype(deleter_)并分配deleter_给该内部对象.但显然这不是正在发生的事情.你能用最小的代码示例来解释这背后的机制吗?

Pra*_*ian 37

一个unique_ptr必须存储其删除器.现在,如果删除器是没有状态的类类型,则unique_ptr可以使用空基优化,以便删除器不使用任何额外空间.

如何完成这一点在实现之间有所不同.例如,libc ++和MSVC都将托管指针和删除器存储在压缩对中,如果涉及的类型之一是空类,它会自动为您提供空基优化.

从上面的libc ++链接

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_;
Run Code Online (Sandbox Code Playgroud)

的libstdc ++ 存储这两种std::tuple和一些谷歌的搜索表明其tuple实现采用空基地优化,但我无法找到任何文件,述明如此明确.

在任何情况下,此示例都演示了libc ++和libstdc ++都使用EBO来减小unique_ptr带有空删除器的a的大小.

  • @Deduplicator我想你可以有一个满足*NullablePointer*的空类型,也可以作为无操作删除器.它可能是有史以来最无用的`unique_ptr`,但Yakk的评论适用于那种情况:) (3认同)
  • 请注意,如果指针是与`deleter_type`相同类型的空对象,则上述代码将无法压缩它们.除了只有一个git会这样做.;) (2认同)

Sir*_*Guy 19

如果删除器是无状态的,则不需要存储空间.如果删除器不是无状态的,那么状态需要存储在unique_ptr自身中.
std::function和函数指针具有仅在运行时可用的信息,因此必须与对象本身的指针一起存储在对象中.这又需要分配(在其unique_ptr自身中)空间来存储该额外状态.

也许了解空基优化将帮助您了解如何在实践中实现这一点.
std::is_empty型特点是如何实现这一点的另一种可能性.

图书馆作者如何实现这一点显然取决于他们以及标准允许的内容.


Bo *_*son 15

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;

};
Run Code Online (Sandbox Code Playgroud)

_UniquePtrImpl类包含指针,从派生deleter_type.

如果删除器恰好是无状态的,则可以优化基类,使其自身不占用任何字节.然后整个unique_ptr可以与包含的指针大小相同 - 即:与普通指针相同的大小.


Kyl*_*and 6

事实上,有是一个大小惩罚lambda表达式是不是无状态的,即lambda表达式捕获一个或多个值.

但对于非捕获lambda,有两个关键事实要注意:

  • lambda的类型是唯一的,只有编译器知道.
  • 非捕获的lambdas是无国籍的.

因此,编译器能够纯粹根据其类型调用lambda ,它被记录为类型的一部分unique_ptr; 不需要额外的运行时信息.

这实际上是为什么非捕获lambda是无国籍的.就大小惩罚问题而言,与任何其他无状态删除函子类型相比,非捕获lambdas当然没有什么特别之处.

请注意,std::function不是无状态的,这就是为什么同样的道理也并不适用于它.

最后,要注意的是,尽管无状态的对象通常需要具有非零大小以确保它们具有唯一的地址,无国籍基类需要添加到派生类型的总大小; 这称为空基优化.因此unique_ptr可以实现(如在Bo Perrson的答案中)作为从删除类型派生的类型,如果它是无状态的,则不会造成大小惩罚.(事实上​​,这可能是正确实施而没有无状态删除者的大小惩罚的唯一方法unique_ptr,但我不确定.)

  • libstdc ++方法是使用std :: tuple开箱即用的空基优化,而不是在任何地方明确地堆积空基类. (2认同)