C++ 智能指针的删除器
为什么要设置删除器
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
//用户代码
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 constexpr(虽然是 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. 标准库为智能指针提供了两个默认版本的删除器,可简化智能指针的代码编写
补充
很多文章中提到 shared_ptr 对数组形式指针的支持需要 C++17 之后,也就是说形如 shared_ptr
经过实验发现,我手上的三份标准库实现(llvm-mingw、MSVC、GCC)在设置语言标准为 C++14 的情况下 shared_ptr<_Tp[]> 的版本都能通过编译,并且只要在构造时传入 new[] 得到的指针,在 shared_ptr 析构时,也都能正确调用 delete[] 释放指针。不知是不是各家标准库为方便用户编码,对标准库进行了改造。
make_shared 函数(C++ 推荐的创建 shared_ptr 的途径)的数组形式(形如 make_shared
上文提到 unique_ptr 针对数组形式实参的偏特化版本没有 operator-> 而是以 operator[] 替代之。在翻阅源码时发现 shared_ptr 也针对数组形式定义了 operator[],不同的是,如果不是以数组形式实例化,调用 shared_ptr 的 operator[] 会直接报错。
MSVC 和 llvm-mingw 均通过 enable_if 保证只有以数组类型实例化版本的 operator[] 为良构。其中MSVC 标准库的 shard_ptr 只要使用数组形式实例化就支持 operator[],未见到区分语言标准的预处理选项;而llvm-mingw 版本从 C++17 之后才加入 shared_ptr 的 operator[]。
GCC 在 shared_ptr 的基类 __shared_ptr 上又封装了一个基类 __shared_ptr_access,对内部指针的访问均通过该基类进行。该类只有针对数组形式的特化版本才定义有 operator[],自 C++17 之后此特化版本更是直接移除了 operator-> 和 operator*,只能使用 operator[] 访问数据。

浙公网安备 33010602011771号