Why set up a deleter
C++11 adds shared_ptr and unique_ptr of STL, and is already a frequent visitor to our encoding. If you use it, you will naturally understand their deleters. For example, pointers created by many C language libraries (GDAL, GLFW, libcurl, etc.) cannot be simply released using delete. When we want to use smart pointers to manage the resources created by these libraries, we must set up deleters:
//Use the class that overloaded operator() as the deleterstruct CurlCleaner { void operator()(CURL *ptr) const { curl_easy_cleanup(ptr); } }; std::unique_ptr<CURL, CurlCleaner> curlu(curl_easy_init(), CurlCleaner{});//The second parameter can be omitted, because CurlCleaner can be constructed by defaultstd::shared_ptr<CURL> curls(curl_easy_init(), CurlCleaner{}); //Use function pointer as deletervoid GLFWClean(GLFWwindow *wnd) { glfwDestroyWindow(wnd); } std::unique_ptr<GLFWwindow, decltype(&GLFWClean)> glfwu(glfwCreateWindow(/*Omitted*/), GLFWClean);//The second parameter must be passed into the actual call function addressstd::shared_ptr<GLFWwindow> glfws(glfwCreateWindow(/*Omitted*/), GLFWClean); //The second parameter in the above two constructors is implicitly converted from function name to function pointer//Use lambda as deleterauto GDALClean=[](GDALDataset *dataset){ GDALClose(dataset); }; std::unique_ptr<GDALDataset, decltype(GDALClean)> gdalu(GDALOpen(/*Omitted*/), GDALClean);//Lambda cannot be constructed by default, an instance must be passed instd::shared_ptr<GDALDataset> gdals(GDALOpen(/*Omitted*/), GDALClean);
The above are the three most commonly used custom deleter forms. You can also use the powerful adaptability of std::function to wrap callable objects as deleter, which will not be expanded here.
The default deleter provided by the standard library
The built-in type and destructor are public class types, and there is no need to specify a deleter. The smart pointer will automatically call delete when the reference count reaches zero to release the managed pointer, making the syntax relatively concise:
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>());
For a long time, I thought that smart pointers only had delete, a default deleter, so every time I managed the pointer obtained by new[], I would write a deleter for it that called delete[] until I looked through the source code of the smart pointer and found that their default deleter actually had a specialized version for array-form pointers:
template<class _Tp> struct default_delete//Default deleter main template{ ... void operator()(_Tp *_Ptr) const noexcept { ... delete _Ptr;//Use delete to release the pointer } ... } template<class _Tp> struct default_delete<_Tp[]>//Specialized version for array form{ ... void operator()(_Tp *_Ptr) const noexcept { ... delete[] _Ptr;//Use delete[] to release the pointer } ... } //unique_ptr template<class _Tp, class _Dp = default_delete<_Tp>/*Default deleter*/> class unique_ptr{...}; //shared_ptr template <class, class _Yp>//Auxiliary class main template, normal pointer applies this versionstruct __shared_ptr_default_delete : default_delete<_Yp> {}; template <class _Yp, class _Un, size_t _Sz>//Array form specialization, matching fixed-length array form, such as std::shared_ptr<int[10]>struct __shared_ptr_default_delete<_Yp[_Sz], _Un> : default_delete<_Yp[]> {}; template <class _Yp, class _Un>//Array form specialization, matching array forms of uncertain length, such as std::shared_ptr<int[]>struct __shared_ptr_default_delete<_Yp[], _Un> : default_delete<_Yp[]> {}; template<class _Tp> class shared_ptr { ... template <class _Yp,/* Check whether the _Yp pointer can be converted to a _Tp pointer (such as a subclass pointer to a base class pointer), and whether the _Yp type can be used for delete and delete[] operations*/> explicit shared_ptr(_Yp* __p) : __ptr_(__p) { ... typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>/*Select the appropriate default deleter based on _Yp type*/, _AllocT> _CntrlBlk; __cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT()); ... } ... }; //User Codestd::unique_ptr<int[]> piu(new int[10]);//Match the int[] version, the deleter compiles to use delete[] to release the pointerstd::shared_ptr<int[]> pis(new int[10]);//Select to use within the constructordelete[]Release pointer deleter
The above code is excerpted from the standard library of llvm-mingw. I checked the implementation of several versions of the standard library at hand and found that the implementation of unique_ptr is roughly similar. It is worth mentioning that unique_ptr itself also has a specialized version unique_ptr<_Tp[]> for array form. Since we know that we manage pointers in array form, this specialized version does not provide operator-> access symbols, but instead operator[] to access array data.
The default deleter selection of llvm-mingw shared_ptr is achieved through the specialization of the auxiliary template class __shared_ptr_default_delete; the constructor of shared_ptr in the MSVC version directly uses if consexpr (although it was supported in C++17, it was found that it has been used in the code of C++14) to determine whether the instantiated pointer type is an array form and select the corresponding deleter; the shared_ptr logic of GCC is relatively complex, and its shared_ptr inherits from __shared_ptr, while __shared_ptr has a member of type _M_refcount. This class has a series of overloaded constructors, several of which are:
struct __sp_array_delete { template<typename _Yp> void operator()(_Yp *__p) const { delete[] __p; } }; r1: template<typename _Ptr>/*Default delete version deleter, omit implementation*/ explicit __shared_count(_Ptr __p) r2: template<typename _Ptr>/*Delegate to 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)/*You can specify the version of deleter and memory allocator*/ r4: template<typename _Ptr>/*Delegate to r3*/ __shared_count(_Ptr __p, true_type) : __shared_count(__p, __sp_array_delete{}, allocator<void>()){} // The constructor of __shared_ptr accepts a pointer parametertemplate<typename _Yp, /* Check _Yp * whether it is converted to instantiated pointer type of class*/> explicit __shared_ptr(_Yp *__p): _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()){...}
Through the code, we can roughly infer that the __shared_count class is a class used to manage reference counts and deleters.
It can be seen that if the pointer type accepted by the __shared_ptr constructor is a normal pointer, __shared_count(__p, false_type) will be called to construct _M_refcount as a version that uses delete to release the pointer; and when the pointer type it accepts is an array-form pointer, __shared_count(__p, true_type) will be called. The deleter stored in the constructed _M_refcount is the __sp_array_delete type, which uses delete[] to release the pointer.
Summarize
1. The type of additional resource release operation is required before destruction. When using smart pointer management, you must set up a custom deleter.
2. The standard library provides two default versions of deleters for smart pointers, which can simplify the code writing of smart pointers.
This is the end of this article about the deleter of C++ smart pointer. For more related content of C++ smart pointer, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!