为什么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. modelform和modelserializer

    modelform modelform比form强悍很多

  2. (转)spring boot整合redis

    一篇写的更清晰的文章,包括redis序列化:http://makaidong.com/ncjava/330749_5285125.html 1.项目目录结构 2.引入所需jar包 <!-- Sp ...

  3. Py-lamda表达式学习【转载】

    转自:https://blog.csdn.net/zjuxsl/article/details/79437563 1.语法定义 在Python中,lambda的语法是唯一的.其形式如下: lambda ...

  4. 梯度下降法实现-python[转载]

    转自:https://www.jianshu.com/p/c7e642877b0e 梯度下降法,思想及代码解读. import numpy as np # Size of the points dat ...

  5. 常用软件安装及VS插件工具

    常用开发工具安装 开发环境 Visual Studio 2013 Microsoft SQL Server 2008 源代码管理 Git TortoiseGit GitScc Provider Cru ...

  6. 【剑指Offer学习】【面试题3 :二维数组中的查找】

    package 二维数组查找; public class Test03 { /** * 在一个二维数组中,每一行都按 package 二维数组查找; public class Test03 { /** ...

  7. #C++初学记录(算法考试1)

    B - Maximal Continuous Rest Each day in Berland consists of n hours. Polycarp likes time management. ...

  8. 20145319 《网络渗透》URL攻击

    20145319 <网络渗透>URL攻击 实验步骤 首先启动apache2,打开我们的钓鱼网页,键入命令/etc/init.d/apache2 start 在浏览器中尝试着访问自己的ip地 ...

  9. 如何写出格式优美的javadoc?

    如果你读过Java源码,那你应该已经见到了源码中优美的javadoc.在eclipse 中鼠标指向任何的公有方法都会显示出详细的描述,例如返回值.作用.异常类型等等. 本文主要来自<Thinki ...

  10. UVa 11082 Matrix Decompressing - 网络流

    开始眨眼一看怎么也不像是网络流的一道题,再怎么看也觉得像是搜索.不过虽然这道题数据范围很小,但也不至于搜索也是可以随随便便就可以过的.(不过这道题应该是special judge,因为一题可以多解而且 ...