Chapter 9 : Introduction To Template Specialization In C++

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 simpledelete’ 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! 🙂

Leave a Reply