为什么unique_ptr的Deleter是模板类型参数,而shared_ptr的Deleter不是?

template <class T, class D = default_delete<T>>
class unique_ptr {
public:
...
unique_ptr (pointer p,
typename conditional<is_reference<D>::value,D,const D&> del) noexcept;
...
}; template <class T>
class shared_ptr {
public:
...
template <class U, class D>
shared_ptr (U* p, D del);
...
};

上面的代码中能看到unique_ptr的第二个模板类型参数是Deleter,而shared_ptr的Delete则只是构造函数参数的一部分,并不是shared_ptr的类型的一部分。

为什么会有这个区别呢?

答案是效率。unique_ptr的设计目标之一是尽可能的高效,如果用户不指定Deleter,就要像原生指针一样高效。

Deleter作为对象的成员一般会有哪些额外开销?

  1. 通常要存起来,多占用空间。
  2. 调用时可能会有一次额外的跳转(相比deletedelete[])。

shared_ptr总是要分配一个ControlBlock的,多加一个Deleter的空间开销也不大,第一条pass;shared_ptr在析构时要先原子减RefCount,如果WeakCount也为0还要再析构ControlBlock,那么调用Deleter析构持有的对象时多一次跳转也不算什么,第二条pass。

既然shared_ptr并不担心Deleter带来的额外开销,同时把Deleter作为模板类型的一部分还会导致使用上变复杂,那么它只把Deleter作为构造函数的类型就是显然的事情了。

unique_ptr采用了“空基类”的技巧,将Deleter作为基类,在用户不指定Deleter时根本不占空间,第一条pass;用户不指定Deleter时默认的Deleter会是default_delete,它的operator()在类的定义内,会被inline掉,这样调用Deleter时也就没有额外的开销了,第二条pass。

因此unique_ptr通过上面两个技巧,成功的消除了默认Deleter可能带来的额外开销,保证了与原生指针完全相同的性能。代价就是Deleter需要是模板类型的一部分。

相关文档

unique_ptr是如何使用空基类技巧的

我们参考clang的实现来学习一下unique_ptr使用的技巧。

template <class _Tp, class _Dp = default_delete<_Tp> >
class 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_;
...
};

忽略掉unique_ptr中的各种成员函数,我们看到它只有一个成员变量__ptr__,类型是__compressed_pair<pointer, deleter_type>。我们看看它是什么,是怎么省掉了Deleter的空间的。

template <class _T1, class _T2>
class __compressed_pair
: private __libcpp_compressed_pair_imp<_T1, _T2> {
...
};

__compressed_pair没有任何的成员变量,就说明它的秘密藏在了它的基类中,我们继续看。

template <class _T1, class _T2, unsigned = __libcpp_compressed_pair_switch<_T1, _T2>::value>
class __libcpp_compressed_pair_imp;

__libcpp_compressed_pair_imp有三个模板类型参数,前两个是传入的_T1_T2,第三个参数是一个无符号整数,它是什么?我们往下看,看到了它的若干个特化版本:

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 0>
{
private:
_T1 __first_;
_T2 __second_;
...
}; template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 1>
: private _T1
{
private:
_T2 __second_;
...
}; template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 2>
: private _T2
{
private:
_T1 __first_;
...
}; template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 3>
: private _T1,
private _T2
{
...
};

看起来第三个参数有4种取值,分别是:

  • 0: 没有基类,两个成员变量。
  • 1: 有一个基类_T1,和一个_T2类型的成员变量。
  • 2: 有一个基类_T2,和一个_T1类型的成员变量。
  • 3: 有两个基类_T1_T2,没有成员变量。

__compressed_pair继承自__libcpp_compressed_pair_imp<_T1, _T2>,没有指定第三个参数的值,那么这个值应该来自__libcpp_compressed_pair_switch<_T1, _T2>::value。我们看一下__libcpp_compressed_pair_switch是什么:

template <class _T1, class _T2, bool = is_same<typename remove_cv<_T1>::type,
typename remove_cv<_T2>::type>::value,
bool = is_empty<_T1>::value
&& !__libcpp_is_final<_T1>::value,
bool = is_empty<_T2>::value
&& !__libcpp_is_final<_T2>::value
>
struct __libcpp_compressed_pair_switch; template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, false> {enum {value = 0};}; template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, true, false> {enum {value = 1};}; template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, true> {enum {value = 2};}; template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, false, true, true> {enum {value = 3};}; template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, true, true, true> {enum {value = 1};};

__libcpp_compressed_pair_switch的三个bool模板参数的含义是:

  1. _T1_T2在去掉顶层的constvolatile后,是不是相同类型。
  2. _T1是不是空类型。
  3. _T2是不是空类型。

满足以下条件的类型就是空类型:

  1. 不是union;
  2. 除了size为0的位域之外,没有非static的成员变量;
  3. 没有虚函数;
  4. 没有虚基类;
  5. 没有非空的基类。

可以看到,在_T1_T2不同时,它们中的空类型就会被当作__compressed_pair的基类,就会利用到C++中的“空基类优化“。

那么在unique_ptr中,_T1_T2都是什么呢?看前面的代码,_T1就是__pointer_type<_Tp, deleter_type>::type,而_T2则是Deleter,在默认情况下是default_delete<_Tp>

我们先看__pointer_type是什么:

namespace __pointer_type_imp
{ template <class _Tp, class _Dp, bool = __has_pointer_type<_Dp>::value>
struct __pointer_type
{
typedef typename _Dp::pointer type;
}; template <class _Tp, class _Dp>
struct __pointer_type<_Tp, _Dp, false>
{
typedef _Tp* type;
}; } // __pointer_type_imp template <class _Tp, class _Dp>
struct __pointer_type
{
typedef typename __pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type type;
};

可以看到__pointer_type<_Tp, deleter_type>::type就是__pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type。这里我们看到了__has_pointer_type,它是什么?

namespace __has_pointer_type_imp
{
template <class _Up> static __two __test(...);
template <class _Up> static char __test(typename _Up::pointer* = 0);
}

简单来说__has_pointer_type就是:如果_Up有一个内部类型pointer,即_Up::pointer是一个类型,那么__has_pointer_type就返回true,例如pointer_traits::pointer,否则返回false

大多数场景下_Dp不会是pointer_traits,因此__has_pointer_type就是false__pointer_type<_Tp, deleter_type>::type就是_Tp*,我们终于看到熟悉的原生指针了!

_T1是什么我们已经清楚了,就是_Tp*,它不会是空基类。那么_T2呢?我们看default_delete<_Tp>

template <class _Tp>
struct default_delete
{
template <class _Up>
default_delete(const default_delete<_Up>&,
typename enable_if<is_convertible<_Up*, _Tp*>::value>::type* = 0) _NOEXCEPT {}
void operator() (_Tp* __ptr) const _NOEXCEPT
{
static_assert(sizeof(_Tp) > 0, "default_delete can not delete incomplete type");
static_assert(!is_void<_Tp>::value, "default_delete can not delete incomplete type");
delete __ptr;
}
};

我们看到default_delete符合上面说的空类型的几个要求,因此_T2就是空类型,也是__compressed_pair的基类,在”空基类优化“后,_T2就完全不占空间了,只占一个原生指针的空间。

而且default_delete::operator()是定义在default_delete内部的,默认是inline的,它在调用上的开销也被省掉了!

遗留问题

  1. __libcpp_compressed_pair_switch_T1_T2类型相同,且都是空类型时,为什么只继承自_T1,而把_T2作为成员变量的类型?
  2. unique_ptrpointer_traits是如何交互的?

C++:为什么unique_ptr的Deleter是模板类型参数,而shared_ptr的Deleter不是?的更多相关文章

  1. std::shared_ptr之deleter的巧妙应用

    本文由作者邹启文授权网易云社区发布. std::shared_ptr 一次创建,多处共享,通过引用计数控制生命周期. 实例 在邮箱大师PC版中,我们在实现搜索时,大致思路是这样的: 每一个账号都有一个 ...

  2. 智能指针unique_ptr

    转自:https://www.cnblogs.com/DswCnblog/p/5628195.html 成员函数 (1) get 获得内部对象的指针, 由于已经重载了()方法, 因此和直接使用对象是一 ...

  3. [C++ Primer] : 第16章: 模板与泛型编程

    面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况, 不同之处在于: OOP能处理类型在程序运行之前都未知的情况, 而在泛型编程中, 在编译时就能获知类型了. 函数模板 模板是C++ ...

  4. C++11 新特性之智能指针(shared_ptr, unique_ptr, weak_ptr)

    这是C++11新特性介绍的第五部分,涉及到智能指针的相关内容(shared_ptr, unique_ptr, weak_ptr). shared_ptr shared_ptr 基本用法 shared_ ...

  5. C++2.0新特性(八)——<Smart Pointer(智能指针)之unique_ptr>

    一.概念介绍 unique_ptr它是一种在异常发生时可帮助避免资源泄露的smart pointer,实现了独占式拥有的概念,意味着它可确保一个对象和其他相应资源在同一时间只被一个pointer拥有, ...

  6. C++11智能指针之std::unique_ptr

    C++11智能指针之std::unique_ptr   uniqut_ptr是一种对资源具有排他性拥有权的智能指针,即一个对象资源只能同时被一个unique_ptr指向. 一.初始化方式 通过new云 ...

  7. c++智能指针(unique_ptr 、shared_ptr、weak_ptr、auto_ptr)

    一.前序 什么是智能指针? ——是一个类,用来存储指针(指向动态分配对象也就是堆中对象的的指针). c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写 ...

  8. 【c++ Prime 学习笔记】第16章 模板与泛型编程

    面向对象编程(OOP)和泛型编程(GP)都能处理在编写程序时类型未知的情况 OOP能处理运行时获取类型的情况 GP能处理编译期可获取类型的情况 标准库的容器.迭代器.算法都是泛型编程 编写泛型程序时独 ...

  9. C++模板元编程(C++ template metaprogramming)

    实验平台:Win7,VS2013 Community,GCC 4.8.3(在线版) 所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得 ...

随机推荐

  1. Kafka核心组件

    一.Kafka核心组件及工作方式 Producer :消息生产者,就是向kafka broker发消息的客户端 Consumer :消息消费者,向kafka broker取消息的客户端 Topic : ...

  2. 摄影EV值深入研究

    1. 什么是EV值 1.1. EV值定义 EV(Exposure Value),曝光值,是反应曝光量的一个值.当感光度为ISO 100.光圈值为F1.曝光时间为1秒时,定义曝光量为0.曝光量减少一档时 ...

  3. Catch all the latest Jordan Release Dates

    In case y'all missed yesterday's news, Air Jordan 13 Olive 2018 officially unveiled their 2017 Holid ...

  4. 7.8 Models -- The Rest Adapter

    一.概述 默认的,store将会使用 DS.RESTAdapter来加载和存储records.这个RESTAdapter假定URLS和JSON关联每一个model是约定好的:这意味着,如果你遵循这个规 ...

  5. Mybatis—三剑客之generator使用方法

    三剑客之generator主要用于自动生成POJO实体类   准备素材: mybatis-generator-core-1.3.2.jar     mysql-connector-java-5.1.2 ...

  6. linux常用命令:find 命令参数详解

    find一些常用参数的一些常用实例和一些具体用法和注意事项. 1.使用name选项: 文件名选项是find命令最常用的选项,要么单独使用该选项,要么和其他选项一起使用.  可以使用某种文件名模式来匹配 ...

  7. shell 命令总结

    删除0字节文件 find . -type f -size 0 -exec rm -rf {} \; find . type f -size 0 -delete 查看进程,按内存从大到小排列 ps -e ...

  8. linux基础命令---mswap

    mkswap 在Linux设备或者文件中创建交换分区,创建完成之后必须使用swapon来使用它.一般在“/etc/fstab”中有一个交换分区列表,这样开机的时候就可以使用它. 此命令的适用范围:Re ...

  9. mysql-cluster 7.3.5安装部署

    集群环境 管理节点 10.0.0.19 数据节点 10.0.0.12 10.0.0.17 sql节点 10.0.0.18 10.0.0.22 添加mysql用户 groupadd mysql user ...

  10. OpenCV相关网站推荐(Informative websites related to OpenCV)

    原文来自:http://answers.opencv.org/question/69691/informative-websites-related-to-opencv/ i think it wil ...