为什么要设置删除器
C++11 加入STL的 shared_ptr 和 unique_ptr,已经是我们编码的常客了。用的多自然就会了解到它们的删除器,比如很多C语言库(GDAL, GLFW, libcurl等等)创建的指针不能简单的使用 delete 释放,当我们想使用智能指针管理这些库创建的资源时,必须设置删除器:
代码
//使用重载了operator()的类作为删除器
struct CurlCleaner
{
void operator()(CURL *ptr) const
{
curl_easy_cleanup(ptr);
}
};
std::unique_ptr<CURL, CurlCleaner> curlu(curl_easy_init(), CurlCleaner{});//第二个参数可省略,因为CurlCleaner可默认构造
std::shared_ptr<CURL> curls(curl_easy_init(), CurlCleaner{});
//使用函数指针作为删除器
void GLFWClean(GLFWwindow *wnd)
{
glfwDestroyWindow(wnd);
}
std::unique_ptr<GLFWwindow, decltype(&GLFWClean)> glfwu(glfwCreateWindow(/*省略*/), GLFWClean);//第二个参数必须传入实际调用的函数地址
std::shared_ptr<GLFWwindow> glfws(glfwCreateWindow(/*省略*/), GLFWClean);
//上述两个构造函数中的第二个参数都进行了函数名到函数指针的隐式转换
//使用lambda作为删除器
auto GDALClean=[](GDALDataset *dataset){ GDALClose(dataset); };
std::unique_ptr<GDALDataset, decltype(GDALClean)> gdalu(GDALOpen(/*省略*/), GDALClean);//lambda无法默认构造,必须传入一个实例
std::shared_ptr<GDALDataset> gdals(GDALOpen(/*省略*/), GDALClean);
上面是三种最常使用的自定义删除器形式,也可以利用 std::function 的强大适配能力来包装可调用对象作为删除器,此处不展开。
标准库提供的默认删除器
内置类型和析构函数为 public 的类类型,无需指定删除器,智能指针会在引用计数归零时自动调用 delete 对管理的指针进行释放,使得语法相对简洁:
代码
std::unique_ptr<int> pi(new int(42));
std::shared_ptr<float> pf(new float(0.0f));
std::unique_ptr<std::vector<int>> pveci(new std::vector<int>());
std::unique_ptr<std::list<int>> plsti(new std::list<int>());
很长一段时间内,我以为智能指针只有 delete 一个默认的删除器,所以每次在管理 new[] 得到的指针时,都会为它编写调用 delete[] 的删除器,直到翻看智能指针的源码,发现它们的默认删除器其实有一个针对数组形式指针的特化版本: 代码
template<class _Tp>
struct default_delete//默认删除器主模板
{
...
void operator()(_Tp *_Ptr) const noexcept
{
...
delete _Ptr;//使用delete释放指针
}
...
}
template<class _Tp>
struct default_delete<_Tp[]>//针对数组形式的特化版本
{
...
void operator()(_Tp *_Ptr) const noexcept
{
...
delete[] _Ptr;//使用delete[]释放指针
}
...
}
//unique_ptr
template<class _Tp, class _Dp = default_delete<_Tp>/*默认删除器*/>
class unique_ptr{...};
//shared_ptr
template <class, class _Yp>//辅助类主模板,普通指针应用该版本
struct __shared_ptr_default_delete : default_delete<_Yp> {};
template <class _Yp, class _Un, size_t _Sz>//数组形式特化,匹配固定长度的数组形式,如std::shared_ptr<int[10]>
struct __shared_ptr_default_delete<_Yp[_Sz], _Un> : default_delete<_Yp[]> {};
template <class _Yp, class _Un>//数组形式特化,匹配不定长度的数组形式,如std::shared_ptr<int[]>
struct __shared_ptr_default_delete<_Yp[], _Un> : default_delete<_Yp[]> {};
template<class _Tp>
class shared_ptr
{
...
template <class _Yp,/*检查_Yp指针是否可转换为_Tp指针(比如子类指针到基类指针)、_Yp类型是否可应用delete与delete[]操作*/>
explicit shared_ptr(_Yp* __p) : __ptr_(__p) {
...
typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>/*根据_Yp类型选择合适的默认删除器*/, _AllocT> _CntrlBlk;
__cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT());
...
}
...
};
//用户代码
std::unique_ptr<int[]> piu(new int[10]);//匹配int[]版本,删除器编译为使用delete[]释放指针
std::shared_ptr<int[]> pis(new int[10]);//构造函数内选择使用delete[]释放指针的删除器
以上代码节选自 llvm-mingw 的标准库,查看了一下手头上的几个版本的标准库实现,发现 unique_ptr 的实现大致类似。值得一提的是,unique_ptr 自身也有针对数组形式的特化版本 unique_ptr<_Tp[]>,由于知晓管理的是数组形式的指针,这个特化版本不提供 operator-> 访问符号,取而代之的是 operator[] 来访问数组数据。
llvm-mingw shared_ptr 默认删除器的选择是通过辅助模板类 __shared_ptr_default_delete 的特化来实现的;MSVC 版本中 shared_ptr 的构造函数则直接使用 if consexpr(虽然是 C++17 开始支持,但是发现 C++14 版本的代码中已经使用)判断实例化指针类型是否为数组形式选择相应删除器;GCC 的 shared_ptr 逻辑相对复杂一些,其 shared_ptr 继承自 __shared_ptr,而 __shared_ptr 有一个类型为 __shared_count 的成员 _M_refcount, 该类有一系列重载的构造函数,其中几个是: 代码
struct __sp_array_delete
{
template<typename _Yp>
void operator()(_Yp *__p) const
{
delete[] __p;
}
};
r1: template<typename _Ptr>/*默认使用delete版本的删除器,省略实现*/
explicit __shared_count(_Ptr __p)
r2: template<typename _Ptr>/*委托给r1*/
__shared_count(_Ptr __P, false_type) : __shared_count(__p){}
r3: template<typename _Ptr, typename _Deleter, typename _Alloc, typename = typename __not_alloc_shared_tag<_Deleter>::type>
__shared_count(_Ptr __p, _Deleter __d, _Alloc __a)/*可指定删除器、内存分配器的版本*/
r4: template<typename _Ptr>/*委托给r3*/
__shared_count(_Ptr __p, true_type) : __shared_count(__p, __sp_array_delete{}, allocator<void>()){}
//__shared_ptr的接受一个指针参数的构造函数
template<typename _Yp, /*检查_Yp *是否和转换为类的实例化指针类型*/>
explicit __shared_ptr(_Yp *__p): _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()){...}
通过代码我们大致可以推测,__shared_count 这个类是用来管理引用计数和删除器的类。
可以看到,如果 __shared_ptr 构造函数接受的指针类型为普通指针,会调用 __shared_count(__p, false_type) 将 _M_refcount 构造为使用 delete 释放指针的版本;而当它接受的指针类型为数组形式指针时,__shared_count(__p, true_type) 则会被调用,构造的 _M_refcount 存储的删除器是 __sp_array_delete 类型,这个类型使用 delete[] 释放指针。
总结
1.销毁前需要额外资源释放操作的类型,使用智能指针管理时必须设置自定义删除器
2.标准库为智能指针提供了两个默认版本的删除器,可简化智能指针的代码编写