Chapter 8 : The Philosophy Of Generic Programming In C++

Key words: Generic programming in C++, template programming in C++, variadic template, Standards template library (STL) – iterators -containers in C++, introduction to smart pointers, custom unique_ptr, custom make_unique

Topics at a glance:

  • Introduction to generic programming
  • Three stages of template programming
  • Variadic templates and parameter packages
  • Containers for storage
  • The elegance of C++ Standard Template Library;
  • Meet the STL’s three musketeers :
    • containers – iterators – algorithms
  • Let’s write an Array class, make it generic, and support iterators
  • Improvising object initialization using std::initializer_lists
  • Let’s write smarter code using smart pointers – learn the philosophy of unique pointers, implementing your own Unique_Ptr class

This chapter talks about the philosophy behind generic programming in C++ using templates, demonstrates the power of template function, variadic templates (packaged parameters), the fundamental philosophy behind C++ Standard Template Library (STL), Iterators and shows how to implement a basic generic Array class with support for random access iterator that you have defined yourself. Curiosity Overloaded ! Further, I will show you how modern C++’s smart pointers work by implementing our own smart pointers such as unique pointers, shared pointers and weak pointers.

Generic programming in C++

So, let us start with one of the most anticipated chapters in C++. The generic programming with templates. In short there are three ways in which we can use templates in C++.

  1. To make generic functions, using template functions
  2. To make generic classes, using template classes
  3. To make generic concepts in C++ using specialized templates

NOTE: C++ concepts using specialized templates is conveyed in depth by Andrei Alexandrescu in his famous book “Modern C++ Design”.

Template programming in C++

Any Template programming in C++ requires three stages, out of which two stages are essentially done by the programmer(s) and third stage is done by the compiler.

Stage 1:

A programmer writes template code, for his template functions, classes or concepts.

Stage 2:

A programmer writes the client code, where the template code is going to be used in a real case scenario, with concrete types.

Stage 3:

The compiler generates the concrete implementation of the template code, but now working with concrete types and not generic types.


The above picture clearly depicts the three stages of template program. So let us walk through each of the stages.

Stage 1: Writing the template code

Here, a programmer, writes a generic code, where the types are parameterized. Let us see a small function which returns the maximum value of two values belonging to a generic type ‘T’. The most important point here is whatever ‘T’ is, it should support the operator ‘>=‘, that’s all!

1
2
3
4
5
template<typename T>
T get_max( const T a, const T b )
{
	return ((a>=b)?a : b);
}

To understand this code, just try to look at the code by replacing ‘T’ with some concrete types, say int, float or char? Does it make sense now? The function returns maximum of two integers. Similarly, the function can work for two chars or two floats, for that matter.

Stage 2: Programmer writing user code

Here, programmer writes a client code making use of the generic template code. But in this stage programmer should mention the type to be used with the template code, say int, float of char.

Take for example:

1
cout << "max(5, 10) is " << dec << get_max<int>(a, b) << endl;

In this example, get_max<int> specifies the type as ‘int’ i.e. integer. Actually type can automatically be deduced based on the type of actual parameters passed, here ‘a’ and ‘b’, which are both integers. This follows the principle of duck typing.

Stage 3: Compiler generates the code

Here compiler generates the code based on the template from stage 1 and the type(s) specified/deduced in stage 2. For the template code I have depicted in stage 1, and for the specified type I have used in stage 2, C++ compiler will generate a code as follows:

1
2
3
4
int get_max( const int a, const int b )
{
    return ((a>=b)?a : b);
}

The main difference from stage 1 to stage 3 is that all instances of generic type ‘T’ is now replaced with a concrete type ‘int’. This is analogous to ‘find and replace‘ utility of any typical text editors. Don’t you see the resemblance?

So, let us see a simple template-based code for understanding the working of templates.

 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
#include <iostream>

using namespace std;

// stage 1: Write the template code
template<typename T>
T get_max( const T a, const T b )
{
    return ((a>=b)?a : b);
}

int main()
{
    int a, b;
    a = 5;
    b = 10;
    
    // stage 2: First usage with specified type as int
    cout << "max(5, 10) is " << dec << get_max<int>(a, b) << endl;
    
    // stage 2: First usage with specified type as float
    cout << "max(1.25F, -7.86F) is " << dec << get_max<float>(1.25F, -7.86F) << endl;
    
    return 0;
}

I have not mentioned stage 3 in the above code. Of course, I cannot do that as it is done by the compiler internally.

Stage three is particularly important. Here, the compiler generates the code in two variants. First variant by replacing ‘T’s with ‘int’ and a second variant by replacing ‘T’s with ‘float’.

The point is, both the variants will get generated, with the same function name. i.e. ‘get_max’. Actually compiler is generating overloaded functions with name ‘get_max’ for you. Compiler writes the code on your behalf.

Let us see the result now :

max(5, 10) is 10
max(1.25F, -7.86F) is 1.25

Template function can be extended to any user defined types such as arrays, structs, classes etc. Let us see an example with a class.

I have the following class with a facility to find the maximum value using operator ‘>=’ and a friend function for overloading ‘<<’ for printing purposes.

 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
class Integer_Pair
{
private:
    int i_;
    int j_;
public:
    Integer_Pair(const int i = 0, const int j = 0): i_{i}, j_{j}
    {
        cout << "Integer_Pair (" << dec << i_ << ", " << j_ << ") created!" << endl;
    }
    
    bool operator>=(const Integer_Pair& other_integer_pair) const 
    {
        bool result = false;
        
        if(   (i_ >= other_integer_pair.i_) 
            &&(j_ >= other_integer_pair.j_) )
        {
            result = true;
        }
        
        return result;
    }
    
    friend ostream& operator<<(ostream& this_ostream, const Integer_Pair& a);
};

ostream& operator<<(ostream& this_ostream, const Integer_Pair& a)
{
    this_ostream << "(" << dec << a.i_ << ", " << a.j_ << ")";
    return this_ostream;
}

Let us use this class instances on our template function now:

1
2
3
4
Integer_Pair ip_1(1, 2);
Integer_Pair ip_2(3, 4);

cout << "max( " << ip_1 << ", " << ip_2 << " ) is " << get_max<Integer_Pair>(ip_1, ip_2) << endl;

What will be the result ?

Integer_Pair (1, 2) created!
Integer_Pair (3, 4) created!
max( (1, 2), (3, 4) ) is (3, 4)

That’s the advantage of template function get_max. It can adopt to any data type supporting ‘>=’ operation, that’s all!.

Let us see the code in its full glory :

 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
#include <iostream>

using namespace std;

// Stage 1: Write the template code
template<typename T>
T get_max( const T a, const T b )
{
    return ((a>=b)?a : b);
}

class Integer_Pair
{
private:
    int i_;
    int j_;
public:
    Integer_Pair(const int i = 0, const int j = 0): i_{i}, j_{j}
    {
        cout << "Integer_Pair (" << dec << i_ << ", " << j_ << ") created!" << endl;
    }
    
    bool operator>=(const Integer_Pair& other_integer_pair) const 
    {
        bool result = false;
        
        if(   (i_ >= other_integer_pair.i_) 
            &&(j_ >= other_integer_pair.j_) )
        {
            result = true;
        }
        
        return result;
    }
    
    friend ostream& operator<<(ostream& this_ostream, const Integer_Pair& a);
};

ostream& operator<<(ostream& this_ostream, const Integer_Pair& a)
{
    this_ostream << "(" << dec << a.i_ << ", " << a.j_ << ")";
    return this_ostream;
}

int main()
{
    int a, b;
    a = 5;
    b = 10;
    
    // stage 2: First usage with specified type as int
    cout << "max(5, 10) is " << dec << get_max<int>(a, b) << endl;
    
    // stage 2: First usage with specified type as float
    cout << "max(1.25F, -7.86F) is " << dec << get_max<float>(1.25F, -7.86F) << endl;
    
    Integer_Pair ip_1(1, 2);
    Integer_Pair ip_2(3, 4);
    
    cout << "max( " << ip_1 << ", " << ip_2 << " ) is " << get_max<Integer_Pair>(ip_1, ip_2) << endl;
    
    
    return 0;
}

Result:

max(5, 10) is 10
max(1.25F, -7.86F) is 1.25
Integer_Pair (1, 2) created!
Integer_Pair (3, 4) created!
max( (1, 2), (3, 4) ) is (3, 4)

Variadic templates

Remember the famous printf() function of standard C library implemented using variadic arguments. As I have detailed in ‘Chapter 4 : The stacks in C’ these functions are implemented based on stack manipulation, the way in which the stack frames are formed.

In C++, we have an alternative, for variadic arguments. It is variadic templates or packaged parameters with strict type checking enforced by the compiler. In stack manipulation counterpart of ‘C’ this strict type checking cannot be there and is all up to the programmer to enforce this.

Let us implement a simple C++ printf() using variadic templates.

See the code below:

 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
#include <iostream>

using namespace std;

// This function is to stop recursive function calls 
// Finally the arglist will come to an end, i.e. no more arguments
// or, just empty arguments
void printf()
{
    cout << endl;
}

template<typename T, typename... Arglist>
void printf(T head, Arglist... tail)
{
    cout << head;
    // recursively call printf with the rest of the arguments
    printf(tail...);
    
    return;
}

int main()
{
    printf("A Set : {", 1, ", ", 2, ", ", 3, "}");
    
    return 0;
}

See the special ellipsis syntax (…) I have used while specifying Arglist type and how it is used throughout the code. You just need to understand that this is the way for specifying packaged parameters in C++.

The important point here is, from a packaged list, we can only access the first named parameter a.k.a head in the code above. After using the head, to access remaining parameters we have to call the function recursively. There is no way that we can do this without recursive function calls. So the most important question is how to and when to stop this recursion.

For that you can see a concrete implementation of a printf() function that takes no arguments. At the end of recursive iterations, it will eventually comes to an end where there are no more arguments, and at this time C++ invokes the printf() with no arguments and stops this recursion. This step is important when using variadic templates, or programmer should use some other logic like some sort of condition check to identify the end of argument list in the code for stopping the recursion.

Let us see the result:

A Set : {1, 2, 3}

Variadic template facilitate the programmer to pass any type of parameters in any number. Compiler generates the code accordingly based on the types and number of parameters for you in stage 3. Did you observe the printf() usage in main(), where string types, and integer types are given as variadic parameters. I hope you understand how to use, variadic template parameters in C++ now.

Standards template library (STL) – containers – iterators – algorithms in C++

Containers in C++

Containers are there in standard template library (STL) of C++ for storing objects/data. There are different types of containers in C++ STL, based on the manner in which they store objects/data within. It’s just not containers that C++ STL gives us; STL also gives various algorithms to work on these containers such as find, sort, search, reverse, accumulate, finding minimum, maximum etc.  But these algorithms are developed in such a way that they almost have no idea what type of containers they work with, or rather, algorithms work on vectors, maps, sets, forward list, arrays, list, dequeue etc. without bothering much about their internal storage representations. i.e. the way each type of container store data differs entirely from each other. So, the question is then how STL has written algorithms that can work seamlessly with any given type of containers. The answer is iterators.

Iterators are mechanisms or facilities provided by the containers to help navigate and manipulate data stored within. Iterators enforces a contract between containers and the algorithms; Contract establishes pre-defined set of behaviors that each party should adhere to. Example of some behaviors are to navigate from start to the end of container in a specific manner, able to use indexing operation to access a specific data stored within, able to navigate forward or reverse from a location in the container, able to access and change data in a specific location within the container.

If you try to picturize the things I have mentioned above, it will be something like this:

Hope you have understood what I am trying to convey here. So iterators make the job easy for both parties i.e. algorithms and containers, by providing pre-defined set of interfaces with a pre-define behavior attribute. Containers implements these iterators within their class definitions whereas algorithms or any user for that manner can use these iterators without bothering much about the internal storage. Algorithms just understands the behaviors of the iterators and uses them to manipulate the stored data within the containers.

With this information, are you now curious to modify our original Array class to have the power of a typical STL container. i.e. Templates will make our Array class a template class/generic class that can store any types of data other than just integers, and iterators can implement certain behaviors that most of the algorithms are expecting.

So let’s make our template Array class:

Step 1: Revisit the original Array class for storing integers:

  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
#include <iostream>

using namespace std;

class Array
{
public:
    enum class error_code{bad_size = -1, bad_index = -2};
private:
    int *memory;
    int size_;
public:
    explicit Array(int size)
    {
        if(size <= 0)
        {
            cout << "Bad size" << endl;
            throw error_code::bad_size;
        }
        
        size_= size;
        memory = new int[size_];
        
        cout << "Array is created with size " << size_ << endl;
    }
    
    ~Array()
    {
        cout << "Array is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    // copy constructor
    Array(const Array& other_object)
    {
        cout << "Array object's clone is created" << endl;
        size_ = other_object.size_;
        memory = new int[size_];
        
        // start copying byte by byte
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
    }
    
    // copy assignment operator 
    Array& operator=(const Array& other_object)
    {
        if(this == &other_object)
        {
            return *this;
        }
        
        cout << "Array object is being cloned" << endl;
        
        if(size_ < other_object.size_)
        {
            delete []memory;
            memory = new int[other_object.size_];
        }
        
        size_ = other_object.size_;
        // Copy byte by byte to memory
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
        
        return *this;
    }
    
    // move constructor
    Array(Array &&other_object)
    {
        cout << "Move constructor: Array has been moved!" << endl;
        size_ = other_object.size_;
        memory = other_object.memory;
        
        //destroy the contents of other_object
        other_object.size_ = 0;
        other_object.memory = nullptr;
    }
    
    // move assignment operator
    Array& operator=(Array&& other_object)
    {
        cout << "An Array object is moved to this Array object" << endl;
        if(this == &other_object)
        {
            return *this;
        }
        
        size_ = other_object.size_;
        delete []memory; // so important. Release the old memory to heap
        memory = other_object.memory;
        
        // destory the contents of other object
        other_object.size_ = 0;
        other_object.memory = nullptr;
        
        return *this;
    }
    
    // indexing operator 
    int& operator[](const int index)
    {
        if(   ( index >= size_ ) 
            ||( index < 0 ) )
        {
            cout << "Bad index" << endl;
            throw error_code::bad_index;
        }
        
        return memory[index];
    }
    
    int size() const
    {
        return size_;
    }
    
};

Step 2: Now, let us make this class a template class. Use selective find and replace method. Make the ‘memory’ generic to hold any data of generic type ‘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
130
#include <iostream>

using namespace std;

template <typename T>
class Array
{
public:
    enum class error_code{bad_size = -1, bad_index = -2};
private:
    T *memory;
    int size_;
public:
    explicit Array(int size)
    {
        // RAII class invariant check 
		if(size <= 0)
        {
            cout << "Bad size" << endl;
            throw error_code::bad_size;
        }
        
        size_= size;
        memory = new T[size_];
        
        cout << "Array is created with size " << size_ << endl;
    }
    
    ~Array()
    {
        cout << "Array is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    // copy constructor
    Array(const Array& other_object)
    {
        cout << "Array object's clone is created" << endl;
        size_ = other_object.size_;
        memory = new T[size_];
        
        // start copying byte by byte
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
    }
    
    // copy assignment operator 
    Array& operator=(const Array& other_object)
    {
        if(this == &other_object)
        {
            return *this;
        }
        
        cout << "Array object is being cloned" << endl;
        
        if(size_ < other_object.size_)
        {
            delete []memory;
            memory = new T[other_object.size_];
        }
        
        size_ = other_object.size_;
        // Copy byte by byte to memory
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
        
        return *this;
    }
    
    // move constructor
    Array(Array &&other_object)
    {
        cout << "Move constructor: Array has been moved!" << endl;
        size_ = other_object.size_;
        memory = other_object.memory;
        
        //destroy the contents of other_object
        other_object.size_ = 0;
        other_object.memory = nullptr;
    }
    
    // move assignment operator
    Array& operator=(Array&& other_object)
    {
        cout << "An Array object is moved to this Array object" << endl;
        if(this == &other_object)
        {
            return *this;
        }
        
        size_ = other_object.size_;
        delete []memory; // so important. Release the old memory to heap
        memory = other_object.memory;
        
        // destory the contents of other object
        other_object.size_ = 0;
        other_object.memory = nullptr;
        
        return *this;
    }
    
    // indexing operator 
    T& operator[](const int index)
    {
        if(   ( index >= size_ ) 
            ||( index < 0 ) )
        {
            cout << "Bad index" << endl;
            throw error_code::bad_index;
        }
        
        return memory[index];
    }
    
    int size() const
    {
        return size_;
    }
    
};

Step 3: Let us see this generic template class in action: Let us use this template code to generate concrete classes of type Array<int> and Array<doubles>

 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
int main()
{
    Array<int> a(10);
    
    for(int index = 0; index < a.size(); ++index)
    {
        a[index] = index;
    }
    
    for(int index = 0; index < a.size(); ++index)
    {
        cout << "a[" << index << "] : " << a[index] << endl;
    }
    
    cout << "\nTesting Array<double>...\n" << endl; 
    Array<double> d(5);
    double number;
    
    d[0] = 1.25F;
    d[1] = 2.278F;
    d[2] = -5.0F;
    d[3] = 100.798F;
    d[4] = 10.2F;
    
    for(int index = 0; index < d.size(); ++index)
    {
        cout << "d[" << index << "] : " << d[index] << endl;
    }
    
    return 0;
}

Let us see the results:

Array is created with size 10
a[0] : 0
a[1] : 1
a[2] : 2
a[3] : 3
a[4] : 4
a[5] : 5
a[6] : 6
a[7] : 7
a[8] : 8
a[9] : 9
Testing Array<double>...
Array is created with size 5
d[0] : 1.25
d[1] : 2.278
d[2] : -5
d[3] : 100.798
d[4] : 10.2
Array is destroyed
Array is destroyed

See how template class Array works?

We can extend this to hold any types such as structs, pointers etc.

Let us see how we can store a struct shown below of an integer and a char array (c-string) to this Array class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct st
{
    int i;
    char string[10];
    
    st& operator=(const st& st_rhs)
    {
        int index = 0;
        i = st_rhs.i;
        
        for(char c: st_rhs.string)
        {
            string[index++] = c;
        }
    }
};

See, the ‘operator=’ has to be defined for your custom type. Array class cannot define a generic operator=<T> for you. Remember this.

Now, let us see the result of the following code:

 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
int main()
{
    cout << "\nTesting Array<struct st>...\n" << endl;
    
    Array<st> s(2);
    st temp = {1, "Hello"};
    s[0] = temp;
    
    temp.i = 2;
    
    const char *const str = "World";
    // Kindly bear with me. 
    // I have not included any standard 'C' library here :)
    int i = 0;
    for(i = 0; str[i]; ++i)
    {
        temp.string[i] = str[i];
    }
    temp.string[i] = '\0';
    
    s[1] = temp;
    
    for(int index = 0; index < s.size(); ++index)
    {
        cout << "s[" << index << "].i : " << s[index].i << endl;
        cout << "s[" << index << "].string : " << s[index].string << endl;
    }
    
    cout << endl;
    
    return 0;
}

Result:

Testing Array<struct st>...
Array is created with size 2
s[0].i : 1
s[0].string : Hello
s[1].i : 2
s[1].string : World
Array is destroyed

Now, are you convinced about the storage capability of our generic Array<T> class ?

Let us implement iterator support for this Array class. I am going to implement a minimalist variant of random access iterator for Array<T>. Random access iterators are those you can find in std::vector container. Random access iterators behave almost like a raw pointer. Programmer can use these iterators as if they are raw pointers, can use de-reference ‘*’, increment ’++’, decrement ‘–‘, indexing operation’[]’, some basic comparisons such as ‘==’, ‘<’, ‘!=’, ‘>’ etc. So my minimalist random access iterator is going to support all the above mentioned behaviors. Please see the modified Array<T> with support for my variant of a random access iterator:

  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
#include <iostream>

using namespace std;

template<typename T>
class Array
{
public:
    enum class error_code{bad_size = -1, bad_index = -2};
private:
    T *memory;
    int size_;
public:
    explicit Array(int size)
    {
        // RAII class invariant check 
        if(size <= 0)
        {
            cout << "Bad size" << endl;
            throw error_code::bad_size;
        }
        
        size_= size;
        memory = new T[size_];
        
        cout << "Array is created with size " << size_ << endl;
    }
    
    ~Array()
    {
        cout << "Array is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    // copy constructor
    Array(const Array& other_object)
    {
        cout << "Array object's clone is created" << endl;
        size_ = other_object.size_;
        memory = new T[size_];
        
        // start copying byte by byte
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
    }
    
    // copy assignment operator 
    Array& operator=(const Array& other_object)
    {
        if(this == &other_object)
        {
            return *this;
        }
        
        cout << "Array object is being cloned" << endl;
        
        if(size_ < other_object.size_)
        {
            delete []memory;
            memory = new T[other_object.size_];
        }
        
        size_ = other_object.size_;
        // Copy byte by byte to memory
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
        
        return *this;
    }
    
    // move constructor
    Array(Array &&other_object)
    {
        cout << "Move constructor: Array has been moved!" << endl;
        size_ = other_object.size_;
        memory = other_object.memory;
        
        //destroy the contents of other_object
        other_object.size_ = 0;
        other_object.memory = nullptr;
    }
    
    // move assignment operator
    Array& operator=(Array&& other_object)
    {
        cout << "An Array object is moved to this Array object" << endl;
        if(this == &other_object)
        {
            return *this;
        }
        
        size_ = other_object.size_;
        delete []memory; // so important. Release the old memory to heap
        memory = other_object.memory;
        
        // destory the contents of other object
        other_object.size_ = 0;
        other_object.memory = nullptr;
        
        return *this;
    }
    
    // indexing operator 
    T& operator[](const int index)
    {
        if(   ( index >= size_ ) 
            ||( index < 0 ) )
        {
            cout << "Bad index" << endl;
            throw error_code::bad_index;
        }
        
        return memory[index];
    }
    
    int size() const
    {
        return size_;
    }
    
private:
    // struct 'itr' is used for storing iterator values 
    struct itr
    {
        int current_position;
        Array<T> *this_object;
        itr(const int position, Array<T> *object) : 
            current_position{position}, this_object{object}
        {
            // do nothing 
        }
    };
    
public:
    // Array class supports limited random access type iterator features 
    class iterator
    {
    private:
        itr *this_itr;
    public:
        iterator(const int position = 0, Array<T> *object = nullptr) :
            this_itr{new itr(position, object)}
        {
            // do nothing 
            cout << "itr created" << endl;
        }
        
        ~iterator()
        {
            if(this_itr != nullptr)
            {
                cout << "itr destroyed" << endl;
                delete this_itr;
            }
        }
        
        // copy is not allowed for iterator object 
        iterator(const iterator& other) =delete;
        
        // move constructor for iterator object
        iterator(iterator&& other)
        {
            this_itr = other.this_itr;
            other.this_itr = nullptr;
        }
        
        // copy is not allowed for iterator object 
        iterator& operator=(const iterator& other) =delete;
        
        // move assignment for iterator object
        iterator& operator=(iterator&& other)
        {
            this_itr = other.this_itr;
            other.this_itr = nullptr;
            
            return *(this);
        }
        
        // iterator indexing operation 
        T& operator[](const int index)
        {
            return this_itr->this_object->memory[index];
        }
        
        // iterator inequality check operation 
        bool operator!=(const iterator& other) const 
        {
            bool ret = false;
            if(this_itr->current_position != other.this_itr->current_position)
            {
                ret = true;
            }
            
            return ret;
        }
        
        // iterator equality check operation 
        bool operator==(const iterator& other) const 
        {
            return!(this->operator!=(other));
        }
        
        // iterator increment operation analogous to pointer increment
        iterator& operator++()
        {
            this_itr->current_position++;
            return *this;
        }
        
        // iterator decrement operation analogous to pointer decrement
        iterator& operator--()
        {
            this_itr->current_position--;
            return *this;
        }
        
        // iterator dereference operation analogous t pointer dereference 
        T& operator*()
        {
            return this_itr->this_object->memory[this_itr->current_position];
        }
        
        // iterator less than comparison
        bool operator<(const iterator& other) const
        {            
            return (this_itr->current_position < other.this_itr->current_position)? true : false;
        }
        
        // iterator greater than comparison
        bool operator>(const iterator& other) const 
        {
            return !(this->operator<(other));
        }
        
    };
    
    iterator begin()
    {
        return (iterator(0, this)); // return a temp value ( r-value )
    }
    
    iterator end()
    {
        return (iterator(size_, this)); // return a temp value ( r-value )
    }
    
};

See that template class Array<T> now support iterator. ‘iterator’ class is defined inside the Array<T> class. This is a typical example for has a relationship. This is very important to note here. ‘iterators’ are facilities provided by the containers. So it must be a ‘has a’ relationship. Containers themselves are NOT iterators. So it cannot be ‘is a’ relationship between iterator class and container class.

For accessing two special iterator instances, one for accessing the starting position of the Array<T>’s storage and second for the last storage location I have defined two member functions for Array<T>. They are begin() and end() and behaves exactly as their names suggest.

All the behaviors I have mentioned above such as dereference ‘*’, increment ’++’, decrement ‘–‘, indexing operation’[]’, some basic comparisons such as ‘==’, ‘<’, ‘!=’, ‘>’ etc. are implemented by the Array<T>::iterator class. Note that copy operations are deleted and only move is supported for now.

So let us see this in work now.

 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
int main()
{
    Array<int> int_array(10);
    
    cout << "1 : array initialization..." << endl;
    int index = 0;
    for(int &i : int_array)
    {
        i = index++;
    }
    cout << "2: array initialization done!\n" << endl;
    
    cout << "3 : array read..." << endl;
    for(int i : int_array)
    {
        cout << "i: " << i << endl;
    }
    cout << "4 : array read done!\n" << endl;
    
    cout << "5: explicit iterator syntax operations to set array values..." << endl;
    
    index = 0;
    
    // Now let us see '*', '++' and '<' in action with explcit iterator syntax
    Array<int>::iterator itr1 = int_array.begin();
    Array<int>::iterator itr1_end = int_array.end();
    
    for( ; itr1 < itr1_end; ++itr1 )
    {
        *itr1 = index++;
    }
    
    cout << "\n6: explicit iterator syntax operations to read array values..." << endl;
    
    Array<int>::iterator itr2 = int_array.begin();
    Array<int>::iterator itr2_end = int_array.end();
    for( ; itr2 < itr2_end; ++itr2 )
    {
        cout << "*itr2 : " << dec << *itr2 << endl;
    }
    
    cout << "main ends here " << endl;
    
    return 0;
   
}

Let us see the result:

Array is created with size 10
1 : array initialization...
itr created
itr created
itr destroyed
itr destroyed
2: array initialization done!
3 : array read...
itr created
itr created
i: 0
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
i: 7
i: 8
i: 9
itr destroyed
itr destroyed
4 : array read done!
5: explicit iterator syntax operations to set array values...
itr created
itr created
6: explicit iterator syntax operations to read array values...
itr created
itr created
*itr2 : 0
*itr2 : 1
*itr2 : 2
*itr2 : 3
*itr2 : 4
*itr2 : 5
*itr2 : 6
*itr2 : 7
*itr2 : 8
*itr2 : 9
main ends here
itr destroyed
itr destroyed
itr destroyed
itr destroyed
Array is destroyed

See the power of iterators now! The main() function even used the modern C++’s ranged for loop. Ranged for loop in C++, requires the basic behaviors such as get the start position, get the end position, incrementing from start to end, basic comparison to see whether ‘start < end’ or ‘start !- end’ etc.

I will tell you why I have deleted copy semantics in iterator. Before that did you notice something. In the implementation of iterator in Array<T> C++ free store  is involved. Array<T>::iterator’s ‘this_itr’ is a pointer to struct itr shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct itr
{
    int current_position;
    Array<T> *this_object;
    itr(const int position, Array<T> *object) : 
        current_position{position}, this_object{object}
    {
        // do nothing 
    }
};

This structure holds current position (index to memory) and one pointer to the storage container itself i.e. Array<T>. This data has to be newly created every time programmer uses begin and end methods. It also implies it is the user code’s responsibility to release this back to heap. As per RAII, memory should be acquired during initialization itself and should be release back during object destruction.

You can ask me why to go through all such issues with memory ownership, let’s resort to std::unique_ptr. We need to understand that unique_ptrs are modern C++ features, while STL containers, iterators and algorithms are developed long back.  ‘iterators’ doesn’t implement smart pointers within.

The trick here is to mimic unique_ptrs. The member function begin() acts like a factory creating instance of iterator pointing to start, through iterator() constructor. Iterator constructor allocates memory from free store to store struct ‘itr’. This local iterator object containing pointer to ‘itr’ is then returned to the caller by begin. Any return values in C++ functions are simply ‘r-values’. So this will internally move-construct/move assign the iterator object to caller’s iterator object.

The ‘move’ semantics actually moves the complete ownership to caller, while copy does not. That’s the main reason behind deleting copy operations. I am not explicitly using any std::move here. Any return values from a function is a r-value. Once, caller’s instance of iterator object goes out of scope, then the destructor is invoked by C++ automatically releasing the memory that the iterator holds within. Here ‘itr’ structure will get released back to heap. Array<T>’s ‘end’ member function also is implemented as begin, with only difference being the ‘itr’ structure is initialized to point to the last location of Array<T>’s memory.

Some points to note:

  1. Don’t call end() method in condition check of ‘for’ loop, as this will result in multiple  memory creations, unnecessarily and will get destroyed once for loop steps into next iteration as a new ‘itr’ object will get created and moved to caller’s stack.
  2. Do not reuse any iterator once they are used in one for loop. For example ‘++’ inside first for loop, will change the current_position of the ‘itr’ object and if used in another for loop will create issues, as it will not start from ‘0’ and would have already reached the last storage location in first for loop itself.

Improving our Array Class

How inconvenient and unnatural it feels like if you cannot initialize an array using initializer list as follows:

int array[] = {1, 2, 3, 4, 5};

Here, programmer doesn’t have to specify the size of the array as well. An array to store 5 integers will be created and along with that the array will get initialized with the sequence of numbers 1, 2, 3, 4, 5. It’s all automatically done by the compiler. This kind of initialization is acceptable only where a variable is defined (instantiated) and nowhere else in the code.

It will be an obvious thing to do, i.e. to make our Array class to accept a sequence of numbers as its initial values and also programmer doesn’t need to specify size. Also, it’s more natural way of initializing an array. Let us welcome the famous “std::initializer_list” to our class. Here, I will write a new constructor, which will take an std::initializer_list as its only argument. Let us see the modified code:

  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
#include <iostream>
#include <initializer_list>

using namespace std;

template<typename T>
class Array
{
public:
    enum class error_code{bad_size = -1, bad_index = -2};
private:
    T *memory;
    int size_;
public:
    explicit Array( const int size )
    {
        // RAII class invariant check 
        if(size <= 0)
        {
            cout << "Error : Size must be greater than 0" << endl;
            throw(error_code::bad_size);
        }
        size_= size;
        memory = new T[size_];
        
        cout << "Array is created with size " << size_ << endl;
    }
    
    Array(std::initializer_list<T> list) : 
        size_{static_cast<int>(list.size())}, memory{new T[list.size()]}
    {
        // start copying the data from list one by one to Array's memory
        int index = 0;
        for( T val : list)
        {
            memory[index++] = val;
        }
        
        cout << "Array initialized with initializer_list for " << size_ << " members" << endl;
    }
    
    ~Array()
    {
        cout << "Array is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    // copy constructor
    Array(const Array& other_object)
    {
        cout << "Array object's clone is created" << endl;
        size_ = other_object.size_;
        memory = new T[size_];
        
        // start copying byte by byte
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
    }
    
    // copy assignment operator 
    Array& operator=(const Array& other_object)
    {
        if(this == &other_object)
        {
            return *this;
        }
        
        cout << "Array object is being cloned" << endl;
        
        if(size_ < other_object.size_)
        {
            delete []memory;
            memory = new T[other_object.size_];
        }
        
        size_ = other_object.size_;
        // Copy byte by byte to memory
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
        
        return *this;
    }
    
    // move constructor
    Array(Array &&other_object)
    {
        cout << "Move constructor: Array has been moved!" << endl;
        size_ = other_object.size_;
        memory = other_object.memory;
        
        //destroy the contents of other_object
        other_object.size_ = 0;
        other_object.memory = nullptr;
    }
    
    // move assignment operator
    Array& operator=(Array&& other_object)
    {
        cout << "An Array object is moved to this Array object" << endl;
        if(this == &other_object)
        {
            return *this;
        }
        
        size_ = other_object.size_;
        delete []memory; // so important. Release the old memory to heap
        memory = other_object.memory;
        
        // destory the contents of other object
        other_object.size_ = 0;
        other_object.memory = nullptr;
        
        return *this;
    }
    
    // indexing operator 
    T& operator[](const int index)
    {
        if(   ( index >= size_ ) 
            ||( index < 0 ) )
        {
            cout << "Bad index" << endl;
            throw error_code::bad_index;
        }
        
        return memory[index];
    }
    
    int size() const
    {
        return size_;
    }
    
private:
    // struct 'itr' is used for storing iterator values 
    struct itr
    {
        int current_position;
        Array<T> *this_object;
        itr(const int position, Array<T> *object) : 
            current_position{position}, this_object{object}
        {
            // do nothing 
        }
    };
    
public:
    // Array class supports limited random access type iterator features 
    class iterator
    {
    private:
        itr *this_itr;
    public:
        iterator(const int position = 0, Array<T> *object = nullptr) :
            this_itr{new itr(position, object)}
        {
            // do nothing 
            cout << "itr created" << endl;
        }
        
        ~iterator()
        {
            if(this_itr != nullptr)
            {
                cout << "itr destroyed" << endl;
                delete this_itr;
            }
        }
        
        // copy is not allowed for iterator object 
        iterator(const iterator& other) =delete;
        
        // move constructor for iterator object
        iterator(iterator&& other)
        {
            this_itr = other.this_itr;
            other.this_itr = nullptr;
        }
        
        // copy is not allowed for iterator object 
        iterator& operator=(const iterator& other) =delete;
        
        // move assignment for iterator object
        iterator& operator=(iterator&& other)
        {
            this_itr = other.this_itr;
            other.this_itr = nullptr;
            
            return *(this);
        }
        
        // iterator indexing operation 
        T& operator[](const int index)
        {
            return this_itr->this_object->memory[index];
        }
        
        // iterator inequality check operation 
        bool operator!=(const iterator& other) const 
        {
            bool ret = false;
            if(this_itr->current_position != other.this_itr->current_position)
            {
                ret = true;
            }
            
            return ret;
        }
        
        // iterator equality check operation 
        bool operator==(const iterator& other) const 
        {
            return!(this->operator!=(other));
        }
        
        // iterator increment operation analogous to pointer increment
        iterator& operator++()
        {
            this_itr->current_position++;
            return *this;
        }
        
        // iterator decrement operation analogous to pointer decrement
        iterator& operator--()
        {
            this_itr->current_position--;
            return *this;
        }
        
        // iterator dereference operation analogous t pointer dereference 
        T& operator*()
        {
            return this_itr->this_object->memory[this_itr->current_position];
        }
        
        // iterator less than comparison
        bool operator<(const iterator& other) const
        {            
            return (this_itr->current_position < other.this_itr->current_position)? true : false;
        }
        
        // iterator greater than comparison
        bool operator>(const iterator& other) const 
        {
            return !(this->operator<(other));
        }
        
    };
    
    iterator begin()
    {
        return (iterator(0, this)); // return a temp value ( r-value )
    }
    
    iterator end()
    {
        return (iterator(size_, this)); // return a temp value ( r-value )
    }
    
};

Did you notice something in the new constructor other than the initializer list? Now, the class’s size check invariant is no longer required, as it is not possible to create an initializer list with 0 or negative size. ‘std::initializer_list’ is not a std::list of STL. We cannot manipulate the data once initializer list it is created. Because it is a constant list of constant values. That says all. So no need to check size of the initializer_list is > 0 in the constructor.

Let us see how to use this newly added constructor for Array<T>.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main()
{
    Array<int> array = {1, 2, 3, 4, 5};
    
    int i = 0;
    
    for(int val : array)
    {
        cout << "array[" << dec << i << "] : " << val << endl;
    }
    
    return 0;
    
}

See how, we have now initialized the array naturally using an initializer list syntax?

1
Array<int> array = {1, 2, 3, 4, 5};

Let us see the result:

itr created
itr created
array[0] : 1
array[0] : 2
array[0] : 3
array[0] : 4
array[0] : 5
itr destroyed
itr destroyed
Array is destroyed

Satisfied to see the initializer_list in action ?

Introduction to smart pointers

The three main smart pointers that modern C++ provides are:

  1. unique pointer – std::unique_ptr
  2. shared pointer – std::shared_ptr
  3. weak pointer – std::weak_ptr

Smart pointers are introduced for just one purpose. i.e. to manage ownership of the associated memory.

Unique pointers as their name suggest will not allow more than one owner for a memory. For this sole purpose, unique_ptr deletes all copy operations. The only thing unique pointer allows is to transfer ownership from one unique pointer to another, via move semantics. After move the old unique pointer will points to nullptr.

Custom unique_ptr

So let us write our own unique pointer class ‘Unique_Ptr’ which will behave almost like the std::unique_ptr declared in <memory>.

In the first version, I have not implemented support for external deleters. Also, the first version is a single object memory version. i.e. it will not support memory to store array of data/objects. So this version of unique pointer can support single instance storages of fundamental data types, single object instances of any class/struct type.

Let us walk through the code implementing our own Unique_Ptr

  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
    
};

Custom make_unique

Now, from C++ 14 onwards there is a utility template function to create std::unique_ptr named as std::make_unique. I have written a custom make unique function named as make_unique_pointer as below:

1
2
3
4
5
6
// utility for making Unique_Ptr
template<typename T, typename... Arglist>
Unique_Ptr<T> make_unique_pointer(Arglist&&... args)
{
    return(Unique_Ptr<T>(new T(std::forward<Arglist>(args)...)));
}

Please refer to a detailed rhetoric on this custom implementation of make_unique on Herb Sutter’s blog @ https://herbsutter.com/gotw/_102/

See that how perfect forwarding of constructor arguments are made via template variadic arguments and std::forward() function.

The special syntax ‘Arglist&&’ can be treated a universal references (Scott Mayers : https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers)

In such cases, we have to use std::forward because, universal references doesn’t necessarily mean always r-value references. It could be l-value or r-value based on the expression used for initializing these Arglist&&. And here it’s not just one type, it’s a list of types. That’s why I have used the ellipsis notation ‘…’ with Arglist&&.

So now, let us see our Unique_Ptr 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
int main()
{
    Unique_Ptr<int> ptr = make_unique_pointer<int>(1);
    
    cout << "*ptr : " << *ptr << endl;
    
    *ptr = 2;
    
    cout << "*ptr : " << *ptr << endl;
    
    class st
    {
    public:
        int i_;
        int j_;
        int k_;
        st(int i = 0, int j = 0, int k = 0): i_{i}, j_{j}, k_{k} {}
    };
    
    cout << "\nTesting Unique_Ptr on class st..." << endl;
    Unique_Ptr<st> ptr_st = make_unique_pointer<st>(1, 2, 3); 
    
    cout << "ptr_st->k_ : " << ptr_st->k_ << endl;
    cout << "ptr_st->j_ : " << ptr_st->j_ << endl;
    cout << "ptr_st->i_ : " << ptr_st->i_ << endl;
    
    return 0;
}

The code is so obvious. It uses the Unique_Ptr instances for integer and a class type as if they are just regular pointers. For member selection operator‘->’, Unique_Ptr class member function operator-> just return a regular pointer type. Compiler will make sure the correct member will get accessed via this raw pointer.

So let us see the result:

1
2
3
4
5
6
*ptr : 1
*ptr : 2
Testing Unique_Ptr on class st...
ptr_st->k_ : 3
ptr_st->j_ : 2
ptr_st->i_ : 1

We will improve this version of Unique_Ptr in the next chapter to include:

  1. Custom deleters
  2. Array support

For now, take this implementation of Unique_Ptr for your reference to understand what’s happening under the hood.

Enjoyed the chapter? Let me know in the comments below. Thanks! 🙂

0

2 thoughts on “Chapter 8 : The Philosophy Of Generic Programming In C++

  1. just to understand what you want to say one needs to go through a very long example. this is a pain. you can put your idea with short length example. this chapter talks about generic programming less than basic.

    1+
    1. Thanks for your feedback. I will look into that aspect wherever applicable. In this chapter I have used a custom unique pointer class and a custom Array class that support random access iterators to just show the reader how standard C++ libraries have used template based generic programming to it’s full extend. And as the functionality goes I have still resorted to a simple array class and a unique pointer class. That’s all. I have given simple code for introducing template functions and variadic template functions at the start of this chapter. Other than such instances where certain concepts are just introduced, most of the code you see throughout this blog are realistic examples like custom arrays, custom smart pointers, custom thread safe queues etc. Even in design patterns I have tried the same method. But anyways I take your feedback very seriously and will try to put some simple examples also while explaining concepts. And I really appreciate and respect your comment. Keep reading my blog. Thanks 🙂

      0

Leave a Reply