Key words: Template specialization in C++, custom unique_ptr for array objects, Adding support for deleters in custom unique_ptr
Topics at a glance:
- Introduction to template specialization
- Improvising our Unique_Ptr class from chapter 8 to add
- Array support using template specialization
- default custom deleters using template specialization and default template parameters.
Template specialization in C++
Template specialization is a vast topic for discussion with multiple facets. I will discuss on template specialization selectively. i.e. When a topic needs to be covered and if template specialization is applicable there, I can explain about it with suitable example. So, here also we need a suitable candidate for discussing on template specialization. Our Unique_Ptr class is one. We will improvise this class step by step in iterations, while I explain template specialization.
Custom unique_ptr for array objects
Improvising our Unique_Ptr class: Support for Arrays
Let us improvise our Unique_Ptr class to support arrays. The first version can only support single objects and not arrays of data/objects. This can only be achieved in modern C++ via template specialization.
So what do you mean by template specialization? It’s simple. First there is a normal template code, which is treated as the base template code. Then, we will specialize it. In our case the base template class is:
1 2 | template< typename T> class Unique_Ptr {...} |
We’ve to work on top of this base template class to make it special. Question is how you want this to be special? Base class template parameter list declares ‘T’, so for adding array support there should be a version that supports ‘T[]’, and this is the specialization that we are going to do here.
Let us specialize ‘T’ to ‘T[]’
I have made the necessary modifications to base Unique_Ptr class to support arrays via ‘T[]’.
First, let us revisit Base Unique_Ptr class that support single objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | #include <iostream> using namespace std; // NOTE 1: deleter not supported as of now // NOTE 2: Single Object version template<typename T> class Unique_Ptr { private: T* raw_ptr; public: Unique_Ptr(T* object = nullptr) : raw_ptr{object} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { delete raw_ptr; } } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( ONLY for array version ) /*T& operator[](const int index) { return raw_ptr[index]; }*/ // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { delete old_ptr; } } // NOTE: deleter is yet to be implemented }; |
Now, let us make this base template class Unique_Ptr a special one to support T[]. i.e. Here ‘T’ is specialized to ‘T[]’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | // NOTE 1: deleter not supported as of now // Specialized template version template<class T> class Unique_Ptr<T[]> // Base class being just template<typename T>Unique_Ptr{} { private: T * raw_ptr; // pointer to an array of T's public: Unique_Ptr(T *object= nullptr) : raw_ptr{object} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { delete []raw_ptr; } } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( enabled support for array version ) T& operator[](const int index) { return raw_ptr[index]; } // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { delete old_ptr; } } // NOTE: deleter is yet to be implemented }; |
See the only difference here is ‘T’ from base class has become ‘T[]’. Also I have enabled indexing operator [] support for this specialized template class. Since this new template code is actually a specialized template class, we have to use the special syntax:
1 2 | template<typename T> class Unique_Ptr<T[]>{…} |
Now, let us define an additional make_unique_ptr for supporting array version of Unique_Ptr. Before that let us revisit make_unique_ptr for base template class version once again.
1 2 3 4 5 | template<typename T, typename...Arglist> Unique_Ptr<T> make_unique_ptr(Arglist&&... args) { return Unique_Ptr<T>(new T(std::forward<Arglist>(args))...); } |
Now, let us write a make_unique_ptr() to work with array version of Unique_Ptr.
1 2 3 4 5 | template<typename T, int N> Unique_Ptr<T[]> make_unique_ptr() { return Unique_Ptr<T[]>(new T[N]); } |
Here you can see that second one in the <parameter list> is an actual type and not a typename/class. The type I have used is ‘int’. Since type is already defined, ‘N’ following the ‘int’ cannot be another type. Of course there would not be any reason for that, certainly. Here ‘N’ must be a constexpr, which could be evaluated by the compiler at compile time. And for the usage here an integral (int type) constexpr.
Let us see how our specialized template class and the new make_unique_ptr() can be put to use:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int main() { const int size = 10; Unique_Ptr<int[]> ptr = make_unique_ptr<int, size>(); // Here 'N' is 10 i.e. size for(int index = 0; index < size; ++index) { ptr[index] = index * 10; } for(int index = 0; index < size; ++index) { cout << "ptr[" << dec << index << "] : " << ptr[index] << endl; } return 0; } |
Here ‘N’ is size, 10 an integer constant.
The same can be achieved also as follows:
1 | Unique_Ptr<int[]> ptr(new int[size]); |
Now, let us see the result.
ptr[0] : 0
ptr[1] : 10
ptr[2] : 20
ptr[3] : 30
ptr[4] : 40
ptr[5] : 50
ptr[6] : 60
ptr[7] : 70
ptr[8] : 80
ptr[9] : 90
Now, we have added Array support. There is another limitation for both the classes still. They don’t support deleters.
Adding support for deleters in custom unique_ptr
Unique_Ptrs : Delegating deletions using deleters
The first question is what are deleters. ‘deleters’ are specialized code for cleaning up. But we already have destructors then why to have further specialized code for this. Answer to this depends on the complexity of classes/objects involved here. In some scenarios, simply deleting the object and releasing the memory would not be the only things to take care during object’s life-cycle end. What if there are some other dependents out there who needs to be notified about objects end. It can be any scenario.
The second question is how to add one. It is also not necessary that objects always will have some kind of custom deleters implemented, or even needed. Sometimes they just need the direct and simple ‘delete’ operation to release back memory to free store. In that case we cannot maintain two versions of Unique_Ptr class; one to support direct ‘delete’ and second one to support custom deleters. What we can use here is a default deleter and use this one when we need only a direct ‘delete’ operation. But when we need a custom deletion then we can supply one custom deleter while constructing the Unique_Ptr instance.
How to achieve this? Here comes the next thing about template.
Default template parameters to the rescue
Just like default parameter values that we give in constructors, we can have default parameter values in template parameter list as well. Let us see this in action:
First let us define a template code for a default deleter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // single object deleter version template<typename T> struct default_deleter // default_deleter is a functor or function object { // don't have any private/public/protected data void operator()(T* ptr) { if(ptr != nullptr) // this check is very important { cout << "default_deleter" << endl; // default deleter is as good as a // direct delete on the pointer. // There cannot be any custom cleanup here. delete ptr; } } }; |
This one is for single object version of Unique_Ptr class, as you can already see that its using the single object variant of ‘delete’ operation here.
Now, let us use this in our single object Unique_Ptr class to delegate deletion.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | // An example of default template type. // TP's default 'type' is default_deleter<T> // If provided, a custom deleter TP, user can either // install a functor object or it can be a simple // function pointer which accepts a pointer parameter. template< typename T, typename TP = default_deleter<T> > class Unique_Ptr { private: T* raw_ptr; TP deleter; // will get constructed anyways with default_deleter type public: explicit Unique_Ptr(T* object) : raw_ptr{object} { // do nothing } // constructor which accepts a custom deleter Unique_Ptr(T* object, TP custom_deleter) : raw_ptr{object}, deleter{custom_deleter} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { // call deleter deleter(raw_ptr); } } TP& get_deleter() { return deleter; } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( ONLY for array version ) /*T& operator[](const int index) { return raw_ptr[index]; }*/ // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { deleter(old_ptr); } } }; |
The main difference here is that I have introduced a second parameter in the parameter list – typename ‘TP’ with a default value default_deleter<T>. i.e if no deleters are supplied by the user, then default delete will be used for delegating deletion.
Now, let us implement a similar support for deletion for our array version of Unique_Ptr also. But here there is a difference. If we have already used a default parameter value for one parameter in a template the same cannot be again assigned with another default value. i.e the default values from base template will carry forward to specialize template. Here Unique_Ptr<T> is a base template and Unique_Ptr<T[]> is a specialized template. Here the approach should be slightly different.
Certainly, we will have to make use of the specialized version of default_deleter. i.e default_delter<T[]> instead of base variant default_deleter<T>. But this specialized deleter will get copy constructed in Unique_Ptr’s constructor. Let us see how specialized variant of default_deleter is implemented.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // Array deleter version template<typename T> struct default_deleter<T[]> // default_deleter is a functor or function object { // don't have any private/public/protected data void operator()(T* ptr) { if(ptr != nullptr) // this check is very important { cout << "default_deleter [] " << endl; // default deleter is as good as a // direct delete on the pointer. // There cannot be any custom cleanup here. delete []ptr; } } }; |
Now, let us see how to copy construct this in specialized Unique_Ptr class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | // Array version T[] with custom deleter support template< typename T, typename TP > // Here TP already has the default type as default_deleter<T> class Unique_Ptr<T[], TP> // Must have Unique_Ptr<T> already defined for T[] to work { private: T * raw_ptr; // pointer to an array of T's TP deleter; public: Unique_Ptr(T *object) : raw_ptr{object}, deleter{default_deleter<T[]>()} // Here we copy-construct specialized deleter<T[]> { // do nothing } Unique_Ptr(T* object, TP this_deleter): raw_ptr{object}, deleter{this_deleter} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { //delete []raw_ptr; // see the array version of delete deleter(raw_ptr); } } TP& get_deleter() { return deleter; } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( ONLY for array version ) T& operator[](const int index) { return raw_ptr[index]; } // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { delete old_ptr; } } }; |
So here the actual thing happening under the hood is:
1. ‘TP’ is having a default type in single object version. i.e. its default type is based in ‘T’
2. In Specialized template class, ‘T’ is specialized to ‘T[]’, and ‘TP’ carry forward the default from base class, in effect specializing ‘TP’ to ‘TP[]’. That’s why in Unique_Ptr’s constructor’s member initializer list we are able to use default_deleter<T[]> and not default_deleter<T>
With this, let us now create some custom deleters and see how the delegation of deletion works both with single object version and array version.
Here’s the Unique_Ptr base template class and specialized template class in their full glory supporting custom deleters.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | #include <iostream> using namespace std; // single object deleter version template<typename T> struct default_deleter // default_deleter is a functor or function object { // don't have any private/public/protected data void operator()(T* ptr) { if(ptr != nullptr) // this check is very important { cout << "default_deleter" << endl; // default deleter is as good as a // direct delete on the pointer. // There cannot be any custom cleanup here. delete ptr; } } }; // Array deleter version template<typename T> struct default_deleter<T[]> // default_deleter is a functor or function object { // don't have any private/public/protected data void operator()(T* ptr) { if(ptr != nullptr) // this check is very important { cout << "default_deleter [] " << endl; // default deleter is as good as a // direct delete on the pointer. // There cannot be any custom cleanup here. delete []ptr; } } }; // An example of default template type. // TP's default 'type' is default_deleter<T> // If provided, a custom deleter TP, user can either // install a functor object or it can be a simple // function pointer which accepts a pointer parameter. template< typename T, typename TP = default_deleter<T> > class Unique_Ptr { private: T* raw_ptr; TP deleter; // will get constructed anyways with default_deleter type public: explicit Unique_Ptr(T* object) : raw_ptr{object} { // do nothing } // constructor which accepts a custom deleter Unique_Ptr(T* object, TP custom_deleter) : raw_ptr{object}, deleter{custom_deleter} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { // call deleter deleter(raw_ptr); } } TP& get_deleter() { return deleter; } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( ONLY for array version ) /*T& operator[](const int index) { return raw_ptr[index]; }*/ // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { deleter(old_ptr); } } }; // Array version T[] with custom deleter support template< typename T, typename TP > // Here TP already has the default type as default_deleter<T> class Unique_Ptr<T[], TP> // Must have Unique_Ptr<T> already defined for T[] to work { private: T * raw_ptr; // pointer to an array of T's TP deleter; public: Unique_Ptr(T *object) : raw_ptr{object}, deleter{default_deleter<T[]>()} // Here we copy-construct specialized deleter<T[]> { // do nothing } Unique_Ptr(T* object, TP this_deleter): raw_ptr{object}, deleter{this_deleter} { // do nothing } ~Unique_Ptr() { if(raw_ptr != nullptr) { //delete []raw_ptr; // see the array version of delete deleter(raw_ptr); } } TP& get_deleter() { return deleter; } // delete copy operations Unique_Ptr(const Unique_Ptr& other_unique_ptr) =delete; Unique_Ptr& operator=(const Unique_Ptr& other_unique_ptr) =delete; // Define move operations Unique_Ptr(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; } Unique_Ptr& operator=(Unique_Ptr&& other_unique_ptr) { // transfer the ownership of memory raw_ptr = other_unique_ptr.raw_ptr; other_unique_ptr.raw_ptr = nullptr; deleter = other_unique_ptr.deleter; return *this; } // define basic operations supported by a regular pointer // 1. dereference operation T& operator*() const { return *raw_ptr; } // 2. member selection operation T* operator->() const { return raw_ptr; } // 3. indexing operation ( ONLY for array version ) T& operator[](const int index) { return raw_ptr[index]; } // 4. equality check bool operator==(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr == other_unique_ptr.raw_ptr); } // 5. in-equality check bool operator!=(const Unique_Ptr& other_unique_ptr) const { return!(this->operator==(other_unique_ptr)); } // 6. less than bool operator<(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr < other_unique_ptr.raw_ptr); } // 7. greater than bool operator>(const Unique_Ptr& other_unique_ptr) const { return!(this->operator<(other_unique_ptr)); } // 8. less than or equal bool operator<=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr <= other_unique_ptr.raw_ptr); } // 9. greater than or equal bool operator>=(const Unique_Ptr& other_unique_ptr) const { return(raw_ptr >= other_unique_ptr.raw_ptr); } T* get() const { return raw_ptr; } explicit operator bool() const { return (raw_ptr != nullptr); } T* release() { T* temp = raw_ptr; raw_ptr = nullptr; return temp; } void reset(T* new_ptr) { T* old_ptr = raw_ptr; raw_ptr = new_ptr; if(old_ptr != nullptr) { delete old_ptr; } } }; |
Now, let us make some custom deleters for both single object and array object variants of Unique_Ptr classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // A custom deleter struct Custom_Deleter { void operator()(int* ptr) { if(ptr != nullptr) { cout << "Custom deleter" << endl; // do some other cleanup if any delete ptr; } } }; // A generic template based custom deleter template<typename G> // G for generic :) (kidding) struct Generic_Custom_Deleter { private: // NOTE: The count_ is just used to identify the // Generic_Custom_Deleter that is called // when a user changes the deleter after // the Unique_Ptr has been already created int count_; public: Generic_Custom_Deleter(int count = 0): count_{count} {} void operator()(G* ptr) { if(ptr != nullptr) { cout << "Generic Custom deleter - " << count_ << endl; // do some other cleanup if any delete ptr; } } }; // A function type deleter void deleter_function(int* ptr) { if(ptr != nullptr) { cout << "Function deleter" << endl; // do some other cleanup if any delete ptr; } return; } struct Custom_Array_Deleter { void operator()(int *ptr) { if(ptr != nullptr) { cout << "Custom_Array_Deleter" << endl; delete[] ptr; // see the array version of delete[] used here } return; } }; |
Now, let us see all this in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | int main() { // local scope 1 { // uses default deleter Unique_Ptr<int> int_ptr_1 = make_unique_ptr<int>(30); cout << "\n*int_ptr_1 : " << dec << *int_ptr_1 << endl; } cout << "\nEnd of local scope 1" << endl; // local scope 2 { Custom_Deleter cd; Unique_Ptr<int, Custom_Deleter> int_ptr_2( new int(50), cd ); cout << "*int_ptr_2 : " << *int_ptr_2 << endl; } cout << "\nEnd of local scope 2" << endl; // local scope 3 { Generic_Custom_Deleter<int> gcd; Unique_Ptr<int, Generic_Custom_Deleter<int>> int_ptr3(new int(60), gcd); cout << "*int_ptr3 : " << *int_ptr3 << endl; Generic_Custom_Deleter<int> gcd_2(2); int_ptr3.get_deleter() = gcd_2; } cout << "\nEnd of local scope 3" << endl; // local scope 4 { Unique_Ptr<int, void(*)(int *)> int_ptr4(new int(70), deleter_function); cout << "*int_ptr3 : " << *int_ptr4 << endl; } cout << "\nEnd of local scope 4" << endl; // local scope 5 { const int size = 10; Unique_Ptr<int[]> int_ptr5 = Unique_Ptr<int[]>(new int[size]); for(int index = 0; index < size; ++index) { int_ptr5[index] = index; } for(int index = 0; index < size; ++index) { cout << "int_ptr5[" << index << "]: " << int_ptr5[index] << endl; } } cout << "\nEnd of local scope 5" << endl; // local scope 6 { const int size = 10; Custom_Array_Deleter cad; Unique_Ptr<int[], Custom_Array_Deleter> int_ptr6 = // Lenghty line Unique_Ptr<int[], Custom_Array_Deleter>(new int[size], cad); for(int index = 0; index < size; ++index) { int_ptr6[index] = index; } for(int index = 0; index < size; ++index) { cout << "int_ptr6[" << index << "]: " << int_ptr6[index] << endl; } } cout << "\nEnd of local scope 6" << endl; return 0; } |
Note that I have used the get_deleter() member function to get and change an already installed deleter in line# 31, 32
Want to see the result?
*int_ptr_1 : 30
default_deleter
End of local scope 1
*int_ptr_2 : 50
Custom deleter
End of local scope 2
*int_ptr3 : 60
Generic Custom deleter - 2
End of local scope 3
*int_ptr3 : 70
Function deleter
End of local scope 4
int_ptr5[0]: 0
int_ptr5[1]: 1
int_ptr5[2]: 2
int_ptr5[3]: 3
int_ptr5[4]: 4
int_ptr5[5]: 5
int_ptr5[6]: 6
int_ptr5[7]: 7
int_ptr5[8]: 8
int_ptr5[9]: 9
default_deleter []
End of local scope 5
int_ptr6[0]: 0
int_ptr6[1]: 1
int_ptr6[2]: 2
int_ptr6[3]: 3
int_ptr6[4]: 4
int_ptr6[5]: 5
int_ptr6[6]: 6
int_ptr6[7]: 7
int_ptr6[8]: 8
int_ptr6[9]: 9
Custom_Array_Deleter
End of local scope 6
So, now we have almost a full fledged Unique_Ptr class with support for arrays and deleters. But this is for understanding the actual working of unique pointers defined by standard C++ library. Always make use of std::unique_ptr in your real code. By the way, if you are using some scaled down C++ without support from standard library packages, or cannot use standard libraries, then you can always try to implement a close to std::unique_ptr using the techniques I have detailed in this chapter.
Enjoyed the chapter? Let me know in the comments below. Thanks! 🙂