Chapter 12 : Series On Design Patterns – Factory Method

Key Words: Design patterns in C++, Factory method in C++, creational design pattern

Topics at a glance:

  • Factory Method and abstracting the creation
  • How to implement Factory method in the modern C++ way

Design patterns in C++

This chapter is the first part of a series on design patterns in C++. There are many important design patterns and so, I have decided to start a series on design patterns. I will discuss about some popular design patterns, their uses, C++ implementation, what all things we need to take care about their thread safety aspects, resource management and ownership aspect.

Creational design pattern: Factory method in C++

In this chapter, we will see factory method pattern, which is a creational design pattern, and what all we need to take care when using factories.

Factory method is simply a static class method that aids in the instantiation of objects particularly belonging to a hierarchical class system. i.e. Base class – derived class hierarchy.

This is important as factory method depends on “is a” relationship that I have mentioned in some previous chapters. i.e. any derived class instance is a base class instance. When it comes to it’s C++ implementation, we need to understand that “is a” identity works only with pointer or reference. For factory method we will work with the former i.e. pointer and not the later i.e. reference.

The example I am illustrating below, has a base-derived class system as depicted below:

Now, we need to write a shape_factory class which defines a static method which will create instances of circle or square depending upon the ‘type’ of shape we pass as parameter to this method.

Now, for fruitful use of this shape factory let us define a client that will test any shapes that it gets by simply drawing it. i.e. Client only bothers about the draw() member function defined for any shape instances.  To make sure any concrete derived classes of base class shape will implement their own variant of draw member function, I have made base class’s draw member function as ‘pure virtual’. Such a base class is also known as an abstract base class in C++. Client make use of this policy. Now, let us see all this in action illustrating a factory method design pattern.

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

using namespace std;

class shape;

enum class shape_type{circle, square};

class shape_factory
{
public:
    static shape* create_shape(shape_type type);
};

// shape is an abstract class
class shape
{
public:
    virtual ~shape()
    {
        // do nothing 
    }
    virtual void draw() const = 0; // pure virtual 
    // cannot create instances of shape, 
    // thus shape is an abstract base class
};

class circle : public shape
{
private:
    float radius_;
public:
    explicit circle(float radius = 0.0F) : radius_{radius}
    {
        // do nothing
    }
    ~circle() 
    { 
        cout << "circle with radius " << radius_ << " destroyed" << endl;
    }
    
    void draw() const override
    {
        cout << "Am a circle with radius " << radius_ << endl;
    }
};

class square : public shape
{
private:
    float length_;
public:
    explicit square(float length = 0.0F) : length_{length} {}
    ~square()
    {
        cout << "square with length " << length_ << " destroyed" << endl;
    }
    void draw() const override
    {
        cout << "Am a square with length " << length_ << endl;
    }
};

shape* shape_factory::create_shape(shape_type type)
{
    switch(type)
    {
        case shape_type::circle:
        {
            float radius;
            
            cout << "Enter the radius for the new circle " << endl;
            cin >> radius;
            if(radius < 0.0F)
            {
                cout << "Bad radius entered. Taking default radius of 0.0F" << endl;
                radius = 0.0F;
            }
            
            return new circle(radius);
        }
        case shape_type::square:
        {
            float length;
            
            cout << "Enter the length for the new square " << endl;
            cin >> length;
            if(length < 0.0F)
            {
                cout << "Bad length entered. Taking default length of 0.0F" << endl;
                length = 0.0F;
            }
            
            return new square(length);
        }
    }// switch type ends here
}

void test_shape(shape_type type)
{
    // create a shape 
    // unique_ptr is very important
    unique_ptr<shape> shape_ptr(shape_factory::create_shape(type));
    // draw the shape
    shape_ptr->draw();
    
    return;
}

int main()
{
    // test circle
    test_shape(shape_type::circle);
    
    // test square
    test_shape(shape_type::square);
    
    cout << "Main ends here" << endl;
    
    return 0;
}

Here, everything is as I have told above, except for the use of unique_ptrs to manage ownership of the created shape instances.

NOTE: Factory method will release the ownership of created object immediately upon return and transfer it to the caller. It is caller’s responsibility to release the object instances’ memory back tofree store.

In this case client is the caller and it manage the ownership elegantly by making use of std::unique_ptr.

Now let us see the result:

Enter the radius for the new circle
1.25
Am a circle with radius 1.25
circle with radius 1.25 destroyed
Enter the length for the new square
7.5
Am a square with length 7.5
square with length 7.5 destroyed
Main ends here

Note that how the shapes are automatically destroyed once the client test_shapes() function returns.

NOTE: If you want shared ownership for the created objects from the factory, then use std::shared_ptr instead of std::unique_ptr.

Factory method hides the creation of objects. Usually client code will be written adhering to code to base class instance principle. Without factory, client will at least have to create specific instances of derived class objects, violating or compromising strict adherence to code to base principle. With factory method, client can fully comply to code to base class methodology.

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

0

Chapter 11 : Threads In C++ And Concurrency

Key Words: Concurrency in C++, Threads in C++, Task, process, thread, std::mutex, std::condition_variable, std::unique_lock, producer-consumer pattern, thread safe queue, thread guards

Topics at a glance:

  • Concurrency achieved via threads – parallel execution units
  • Let’s understand the subtleties : Task, Process and Threads
  • Learn about process abstraction
  • Let’s spawn a process – what are the essential steps that every programmer must understand
  • Threads and their inherent issues – race conditions and same time access
  • Mutex locks and Condition Variables
  • Let’s guard our thread using thread guards
  • How to mitigate issues with concurrent execution using C++ facilities
    • Mutually Exclusive Locks a.k.a mutex locks
    • lock ownership and management in C++
  • Let’s implement a Queue
  • Let’s implement a thread-safe queue – Classic producer-consumer pattern

Concurrency in C++

This chapter introduces the threads in C++, their uses, inherent issues, and how to tackle them.

Threads in C++

So what are threads? Threads are the basic units of execution on any platform. Platform is an abstract term here. Platform can be a full rich operating system, in which case, threads are running on the application layer, a.k.a application threads, on top of kernel doing tasks assigned by the user. Platform can be a kernel of an OS, in which threads are called kernel threads and they do privileged task for the kernel. Platform can be also bare-metal or even a virtual machine. Regardless of the platform, threads are units of execution that performs some pre-defined task.

Process and threads

Here, let us assume that platform is an operating system such as MS Windows or Linux based operating system. OS provides the abstraction called process. Any process requires at least one unit of execution or thread. This thread is called the main thread. The function that runs in the main thread is identified by the user defined main function in the program. Usually the OS creates and spawns a process. The steps are too big and out of scope here. But I will try to cover the basics.

Before covering the steps, let us understand what you mean by a process regardless of an OS or bare-metal or hypervisor (virtual) platform. We all know the actual platform on top of which anything runs/executes is the hardware, comprising of the CPU and some memory system along with some peripherals such as IO devices, network interfaces etc. This hardware platform is not limitless. Typically, there is only one processor (with single or multiple cores), one RAM, and limited number of IO devices and other peripherals. These are the basic things that constitute any hardware platform. So if a task has to run on this HW platform directly other tasks cannot be executed as this running task solely takes the full CPU time and engages the HW fully. A computer will become eventually useless as it has to / can run only a single task all the time. To overcome this, the idea is to assign specific time slots for any given task to run and to leave the platform for other tasks once this slot is exhausted and resume back once a slot is free or a pre-defined slot occurs. This task-to-task switching is difficult if the tasks themselves are let to manage and will not be reliable. What if there is a rogue task who does not want to give the resource back. Also, who will manage the task to task context switching, like saving the CPU register values and critical information required by the task to resume its operation? To mitigate all these issues, we came up with the abstraction called process. Process abstracts the underlying platform (HW or some other SW platform) and gives the task an impression that there is only one task that is using the CPU at any given point of time. Processes will get pre-empted by other processes performing other tasks and will get resumed back in a while. Switching the tasks/processes itself is done by another task and we call this special program the task scheduler or process scheduler.

Any task has two aspects:

  1. The functionality that the programmer(s) define in the source code.
  2. An environment for execution.

Process gives the second i.e. an execution environment to run and helps in making sure that the first aspect, which is the functionality, runs as intended. Execution environment is manifested as an abstract entity that provides the task, resources such as a virtual CPU, memory, stack, files, sockets, IO devices etc. This abstract environment is called the process. Process initiates execution from the main function, after setting up some initial things required for the task to run. Process’s will have one unit of execution as I have mentioned above, that runs the main program.  Any execution unit within a process must require one thing i.e. a dedicated stack. As I said in Chapter 4 : The stacks in ‘C’, any C program or a C++ program for that matter, requires stacks for their execution. So, if you got my point clearly, then you can easily guess that for ‘N’ number of threads (i.e. execution units) we need ‘N’ independent stacks. That said, a process itself will have a pool of stack and for every unit of execution i.e. threads it assigns a dedicated slot(s) from this stack pool. The second most important thing required for execution unit is the virtual platform resources. It requires its own copy of CPU registers without which task-to-task context switching is not possible. Before switching over to other task or process, the running process has to save it’s copy of CPU registers and other critical parameters. These information will be used by the underlying platform to resume back the pre-empted task/process after a while. That said these things are not possible without the process abstraction. Let us summarize the key points related to task, process and threads:

Task, process and threads

  1. Task is just a job defined by a programmer. Say like a recipe for a cuisine, but not the dish itself.
  2. Process provides an abstract execution environment for loading this program and executing it such as setting up independent stacks for independent execution units. Aiding task-to-task context switching by abstracting the CPU registers and other critical resources.This is analogous to getting the ingredients to cook the dish, making the cooking utensils ready etc.
  3. Thread is actual unit of execution. The one that runs on top of CPU. That means now the cook (i.e CPU) has started to make the dish. I.e CPU starts executing the program. The final work accomplished by the running thread is the dish itself! Voila!!!
  4. One important thing to note here is that, although process can provide the bare-minimum execution environments in form of stacks and CPU registers and other critical parameters, threads share all other things of a process, such as data segment, open file handles, sockets device handles etc. Programmatically any thread can access these common resources shared within the same process. This is the most important point that we should focus as this leads to lot of issues with thread programming. Process only guarantees that data/resources within one process is safe from another process. But this protection is not there among threads, as they do share process resources among themselves.

These are the minimum essential points that any programmer should understand. Let it be any platform, any OS, hardware, any programming language.

NOTE: All the things I have mentioned above is not specific to any OS/Platform. I have tried to comprehend the bigger picture as generic as possible.

Now, let us go through the basic steps involved in spawning a process till running a thread on CPU.

  1. Typically, OS will create a new process from a parent process. To easily relate, say you are launching an executable file (*.exe) from Windows command prompt or Linux bash, or say you are launching from a GUI program like Windows Explorer or Linux’s File Managers such as Nautilus. So here, the parent process itself is the launcher (command prompt or windows explorer)
  2. This is known as forking a process. An exact replica of the parent process is created. Now we need to replace the executable in this clone with the program’s executable (say ping.exe the famous packet ping program). OS will do this task next.
  3. Once this is done, the process must start execution. Process must set up the minimum execution environment for the thread(s) to run, as I have detailed above, and jump to the entry point of program. It looks for the keyword _main or something similar and jump to that. This is platform specific.
  4. This combined forking and executing can be called as spawning a process.
  5. From here, the main thread will start execution, underlying platform (typically OS or virtual machine) will do pre-emption, context switching and resumption operations. In course of execution the spawned main thread will result in creating multiple threads within the same process or may even result in spawning multiple processes depending upon the complexity of the program.

There are other aspects that needs to be discussed such as thread to thread synchronization, process to process communication through IPC’s etc. Our topic of interest in this chapter is the thread itself. So, let us now focus much on threads and their attributes.

Mostly threads within a process will be doing one part of a big task and is quite likely to be inter-dependent and not as independent as I have mentioned above. Most of the threads will have to share many resources such as data/memory/files/sockets/devices etc in course of their execution. Since they are all shared among thread within the same process, its quite likely that there can be issues related to same time access. The word ‘same’ is especially important here. Same time access can lead to coherency problems, ownership issues etc. I am not going to point out all such issues here as it is widely out of scope.

Now, let us see how C++ provides the programmer facilities/features to define units of execution called threads. But, of course, process itself is implemented by the OS/Virtual machine underneath. The std::thread helps the programmer to define a parallel executable independent unit within the same process. C++ also provides lot of facilities to mitigate the issues inherent with parallel execution of threads sharing common process resources. These includes the following:

  • atomics
  • mutex locks
  • condition variables and predicates
  • lock management entities such a unique_locks, scoped_locks, shared_locks, deferred locks etc.

So let us understand these things one by one in detail. I will explain these things selectively with relevant example.

Thread safe Queues

I am taking the example of a queue, implemented as a First In First Out or FIFO. The example is iterative.

In the first iteration, I will implement a basic queue with insert and get operations. One thread will add data to it and after that, the same thread will get the data out of it thus demonstrating the operation of the queue.

Now, let us see the basic queue class. This class is defined to support integer data.

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

using namespace std;

class Queue
{
public:
    enum class error_codes{bad_size = -1};
private:
    int *memory;
    int size_;
    int head;
    int tail;
    bool is_full;
    bool is_empty;
public:
    explicit Queue(const int size = 0) : 
        size_{size}, is_full{false}, is_empty{true}
    {
        if(size_ <= 0)
        {
            cout << "Bad size specified" << endl;
            throw error_codes::bad_size;
        }
        
        memory = new int[size_];
        
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is created " << endl;
    }
    
    ~Queue()
    {
        delete []memory;
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is destroyed" << endl;
        size_ = 0;
    }
    
    // delete copy/move operations 
    Queue(const Queue& other_stack) =delete;
    Queue& operator=(const Queue& other_stack) =delete;
    Queue(Queue&& other_stack) =delete;
    Queue& operator=(Queue&& other_stack) =delete;
    
    bool insert(const int data)
    {
        bool ret = true;
        
        if( is_full == false )
        {
            memory[tail] = data;
            tail = (tail + 1) % size_;
            if(tail == head)
            {
                is_full = true;
            }
            is_empty = false;
        }
        else
        {
            // queue is full
            cout << "\tQueue is full : " << data  << endl;
            ret = false;
        }
        
        return ret;
    }
    
    bool get(int * data)
    {
        bool ret = true;
        
        if(is_empty == false)
        {
            *data = memory[head];
            head = (head + 1) % size_;
            is_full = false;
            if(head == tail)
            {
                is_empty = true;
            }
        }
        else
        {
            // queue empty
            cout << "\tQueue is empty" << endl;
            ret = false;
        }
        
        return ret;
    }
    
    int size() const
    {
        return size_;
    }
    
};

Now, let us see some basic queue operations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
    const int q1_size = 10;
    Queue q1(q1_size);
    
    for(int count = 0; q1.insert(count); ++count)
    {
        // do nothing
    }
    
    int val = 0;
    
    for(int count = 0; q1.get(&val); ++count)
    {
        
        cout << "Val popped : " << dec << val << endl;
    }
    
    return 0;
}

Result:

Queue with size : 10 is created
        Queue is full : 10
Val popped : 0
Val popped : 1
Val popped : 2
Val popped : 3
Val popped : 4
Val popped : 5
Val popped : 6
Val popped : 7
Val popped : 8
Val popped : 9
        Queue is empty
queue with size : 10 is destroyed

Producer-Consumer pattern

Now, let’s start the second iteration. In the second iteration, I will make a thread (producer) that generates data and adds them to queue and another thread (consumer) that takes data out of queue and do some processing.

Mutex lock and condition variable

This will introduce an issue. How the consumer know data is ready or not? How the producer knows whether consumer is not taking some data from the queue, while it tries to add new data. This is an example of classic producer-consumer pattern. To tackle these, I will add the following features into our queue class.

  • Mutex lock
  • Condition variable

In simple terms, mutex lock guarantees only one thread can lock the mutex at any given point of time. Any other threads trying to get the lock will be put to sleep. When the thread that locked the mutex releases the lock, other thread(s) sleeping for the mutex will be scheduled again to run in an order. We will protect the critical regions in the code from simultaneous access from multiple threads through mutex locks.

Condition variable, is essentially an atomic variable, used for checking a condition is satisfied or not. Here, the condition is to check whether a new data is available in the queue to process or not. Consumer thread, will check whether the condition is passed or else goes to sleep, while producer thread will make the condition pass and notify any waiting threads. i.e. consumer checks whether new data is ready in queue while producer makes new data available in queue. Condition variables work with mutex, as follows:

  • Consumer thread will get the lock on a mutex first
  • Consumer thread will check the condition has passed or else goes to sleep while releasing the lock
  • Meanwhile, producer thread will lock the mutex and set the condition to pass and notifies any other waiting thread while unlocking the mutex.
  • Waiting/sleeping thread will receive this notification and wakes up locking the mutex and checks the condition is pass or not. If not passed goes back to sleep unlocking the mutex. But if passed perform any work/computation and unlocks the mutex.
  • The same cycle repeats as long as it is required.

All these steps are possible through C++’s std:mutex and std::condition_variable. Along with these two more entities are also there to aid us.

They are std::unlique_lock and predicate.

std::unique_lock and predicates

  • std::unique_locks: I told that std::condition_variables works with mutex locks. But in course of execution a condition variable lock the mutex, unlocks the mutex before going to sleep and locks the mutex when coming back from sleep. Condition variable cannot take complete ownership of the mutex. It just uses the mutex in course of its action and releases it when not needed, such as in a sleep state. So, like how std::unique_ptr’s manages the ownership of raw pointers, unique_locks helps in taking care of the ownership of mutex. It actually abstracts various methods which could be invoked on a mutex such as locking the mutex while the unique_lock instance is created in its constructor itself and unlocking or releasing the mutex while unique_lock instance is going out of scope and is being destructed. Condition variable uses such kinds of managers for managing mutex locks and its not recommended to do this without any such lock managers.
  • Predicates: In English, the logic of a predicate implies something which is affirmed or denied concerning an argument of a proposition. Likewise, for a condition variable to work correctly, there should be a condition which could be evaluated to either true or false. This condition is called predicate. So, in this case, consumer thread wait on a std::condition_variable, with a predicate that, it will wake up ONLY to check whether the condition is true or else it will go back to sleep.

Now, let us put all these and see how we can improvise the queue.

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

using namespace std;

class Queue
{
public:
    enum class error_codes{bad_size = -1};
private:
    int *memory;
    int size_;
    int head;
    int tail;
    bool is_full;
    bool is_empty;
    std::mutex mutex_;
    std::condition_variable cond_var;
    bool data_ready;
public:
    explicit Queue(const int size = 0) : 
        size_{size}, data_ready{false}, is_full{false}, is_empty{true}
    {
        if(size_ <= 0)
        {
            cout << "Bad size specified" << endl;
            throw error_codes::bad_size;
        }
        
        memory = new int[size_];
        
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is created " << endl;
    }
    
    ~Queue()
    {
        delete []memory;
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is destroyed" << endl;
        size_ = 0;
    }
    
    // delete copy/move operations 
    Queue(const Queue& other_queue) =delete;
    Queue& operator=(const Queue& other_queue) =delete;
    Queue(Queue&& other_queue) =delete;
    Queue& operator=(Queue&& other_queue) =delete;
    
    
    bool insert(const int data)
    {
        bool ret = true;
        std::unique_lock<mutex> lck_(mutex_);
        
        if( is_full == false )
        {
            memory[tail] = data;
            tail = (tail + 1) % size_;
            if(tail == head)
            {
                is_full = true;
            }
            is_empty = false;
        }
        else
        {
            // queue is full
            cout << "\tQueue is full : " << data  << endl;
            ret = false;
        }
        
        if(ret == true)
        {
            data_ready = true;
            cond_var.notify_one(); // unlocks the lock within the function 
        }
        
        //lck_.unlock(); // actually unnecessary to do this for unique_lock 
        
        return ret;
    }
    
    bool get(int * data)
    {
        bool ret = true;
        
        unique_lock<mutex> lck(mutex_);
        
        // predicate 'check for data_ready' is VERY IMPORTANT here 
        cond_var.wait(lck, [this]{return this->data_ready;} );
        
        if(is_empty == false)
        {
            *data = memory[head];
            head = (head + 1) % size_;
            is_full = false;
            if(head == tail)
            {
                is_empty = true;
            }
        }
        else
        {
            // queue empty
            cout << "\tQueue is empty" << endl;
            ret = false;
        }
        
        return ret;
    }
    
    int size() const
    {
        return size_;
    }
    
};

void producer(Queue& queue)
{
    bool ret = false;
    
    for(int count = 0; count < 100; ++count)
    {
        while(false == queue.insert(count));            
    }
    
    return;
}

void consumer(Queue& queue)
{
    int val = 0;
    while(true)
    {
        queue.get(&val);
        
        cout << "Val popped : " << val << endl;
        
        if(val == 99) break;
    }
    
    return;
}

Please see the producer thread function, consumer thread function, how they synchronize with one another through the predicate based on data_ready through mutex locks and condition_variables. Understand how std::unique_locks are used to manage mutex object.

Now let us create two threads; a producer thread and a consumer thread and see the queue in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int main()
{    
    Queue q1(10);
    
    // create consumer thread first 
    std::thread consumer_thread(consumer, std::ref(q1));
    
    // create producer thread
    std::thread producer_thread(producer, std::ref(q1));
    // wait for the threads to join back main
    producer_thread.join();
    consumer_thread.join();
    
    cout << "Main ends" << endl;
    
    return 0;
}

Result:

Queue with size : 10 is created
        Queue is full : 10
Val popped : 0
        Queue is full : 11
Val popped : 1
        Queue is full : 12
Val popped : 2
        Queue is full : 13
Val popped : 3
        Queue is full : 14
Val popped : 4
        Queue is full : 15
Val popped : 5
        Queue is full : 16
Val popped : 6
        Queue is full : 17
Val popped : 7
        Queue is full : 18
Val popped : 8
        Queue is full : 19
Val popped : 9
        Queue is full : 20
Val popped : 10
        Queue is full : 21
Val popped : 11
        Queue is full : 22
Val popped : 12
        Queue is full : 23
Val popped : 13
        Queue is full : 24
Val popped : 14
        Queue is full : 25
Val popped : 15
        Queue is full : 26
Val popped : 16
        Queue is full : 27
Val popped : 17
        Queue is full : 28
Val popped : 18
        Queue is full : 29
Val popped : 19
        Queue is full : 30
Val popped : 20
        Queue is full : 31
Val popped : 21
        Queue is full : 32
Val popped : 22
        Queue is full : 33
Val popped : 23
        Queue is full : 34
Val popped : 24
        Queue is full : 35
Val popped : 25
        Queue is full : 36
Val popped : 26
        Queue is full : 37
Val popped : 27
        Queue is full : 38
Val popped : 28
        Queue is full : 39
Val popped : 29
        Queue is full : 40
Val popped : 30
        Queue is full : 41
Val popped : 31
        Queue is full : 42
Val popped : 32
        Queue is full : 43
Val popped : 33
        Queue is full : 44
Val popped : 34
        Queue is full : 45
Val popped : 35
        Queue is full : 46
Val popped : 36
        Queue is full : 47
Val popped : 37
        Queue is full : 48
Val popped : 38
        Queue is full : 49
Val popped : 39
        Queue is full : 50
Val popped : 40
        Queue is full : 51
Val popped : 41
        Queue is full : 52
Val popped : 42
        Queue is full : 53
Val popped : 43
        Queue is full : 54
Val popped : 44
        Queue is full : 55
Val popped : 45
        Queue is full : 56
Val popped : 46
        Queue is full : 57
Val popped : 47
        Queue is full : 58
Val popped : 48
        Queue is full : 59
Val popped : 49
        Queue is full : 60
Val popped : 50
        Queue is full : 61
Val popped : 51
        Queue is full : 62
Val popped : 52
        Queue is full : 63
Val popped : 53
        Queue is full : 64
Val popped : 54
        Queue is full : 65
Val popped : 55
        Queue is full : 66
Val popped : 56
        Queue is full : 67
Val popped : 57
        Queue is full : 68
Val popped : 58
        Queue is full : 69
Val popped : 59
        Queue is full : 70
Val popped : 60
        Queue is full : 71
Val popped : 61
        Queue is full : 72
Val popped : 62
        Queue is full : 73
Val popped : 63
        Queue is full : 74
Val popped : 64
        Queue is full : 75
Val popped : 65
        Queue is full : 76
Val popped : 66
        Queue is full : 77
Val popped : 67
        Queue is full : 78
Val popped : 68
        Queue is full : 79
Val popped : 69
        Queue is full : 80
Val popped : 70
        Queue is full : 81
Val popped : 71
        Queue is full : 82
Val popped : 72
        Queue is full : 83
Val popped : 73
        Queue is full : 84
Val popped : 74
        Queue is full : 85
Val popped : 75
        Queue is full : 86
Val popped : 76
        Queue is full : 87
Val popped : 77
        Queue is full : 88
Val popped : 78
        Queue is full : 89
Val popped : 79
        Queue is full : 90
Val popped : 80
        Queue is full : 91
Val popped : 81
        Queue is full : 92
Val popped : 82
        Queue is full : 93
Val popped : 83
        Queue is full : 94
Val popped : 84
        Queue is full : 95
Val popped : 85
        Queue is full : 96
Val popped : 86
        Queue is full : 97
Val popped : 87
        Queue is full : 98
Val popped : 88
        Queue is full : 99
Val popped : 89
Val popped : 90
Val popped : 91
Val popped : 92
Val popped : 93
Val popped : 94
Val popped : 95
Val popped : 96
Val popped : 97
Val popped : 98
Val popped : 99
Main ends
Queue with size : 10 is destroyed

But here there is a small problem, the main() has to make sure it will wait for the threads, it has spawned to return back. A process cannot exit while the threads are running. C++ make sure the threads are terminated when the process exits. To avoid this, main() waits on the threads to return back by invoking the join method. This should be done carefully every time. Let us see a way to ensure this policy.

Thread guards

Creating thread guards are the way to do this automatically. In the below code I have defined a thread_guard class, that creates the threads in its constructor and joins the thread it created in its destructor, thus making use of the famous C++ RAII principle.

Let us see thread_guard in action. Also, this time I have changed the Queue class to work with generic type ‘T’, making Queue class a template class Queue<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
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
#include <iostream>
#include <thread>
#include <condition_variable>

using namespace std;

template<typename T>
class Queue
{
public:
    enum class error_codes{bad_size = -1};
private:
    T *memory;
    int size_;
    int head;
    int tail;
    bool is_full;
    bool is_empty;
    std::mutex mutex_;
    std::condition_variable cond_var;
    bool data_ready;
public:
    explicit Queue(const int size = 0) : 
        size_{size}, data_ready{false}, is_full{false}, is_empty{true}
    {
        if(size_ <= 0)
        {
            cout << "Bad size specified" << endl;
            throw error_codes::bad_size;
        }
        
        memory = new T[size_];
        
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is created " << endl;
    }
    
    ~Queue()
    {
        delete []memory;
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is destroyed" << endl;
        size_ = 0;
    }
    
    // delete copy/move operations 
    Queue(const Queue& other_queue) =delete;
    Queue& operator=(const Queue& other_queue) =delete;
    Queue(Queue&& other_queue) =delete;
    Queue& operator=(Queue&& other_queue) =delete;
    
    
    bool insert(const T data)
    {
        bool ret = true;
        std::unique_lock<mutex> lck_(mutex_);
        
        if( is_full == false )
        {
            memory[tail] = data;
            tail = (tail + 1) % size_;
            if(tail == head)
            {
                is_full = true;
            }
            is_empty = false;
        }
        else
        {
            // queue is full
            cout << "\tQueue is full : " << data  << endl;
            ret = false;
        }
        
        if(ret == true)
        {
            data_ready = true;
            cond_var.notify_one(); // unlocks the lock within the function 
        }
        
        //lck_.unlock(); // actually unnecessary to do this for unique_lock 
        
        return ret;
    }
    
    bool get(T * data)
    {
        bool ret = true;
        
        unique_lock<mutex> lck(mutex_);
        
        // predicate 'check for data_ready' is VERY IMPORTANT here 
        cond_var.wait(lck, [this]{return this->data_ready;} );
        
        if(is_empty == false)
        {
            *data = memory[head];
            head = (head + 1) % size_;
            is_full = false;
            if(head == tail)
            {
                is_empty = true;
            }
        }
        else
        {
            // queue empty
            cout << "\tQueue is empty" << endl;
            ret = false;
        }
        
        return ret;
    }
    
    int size() const
    {
        return size_;
    }
    
};

template<typename T>
void producer(Queue<T>& queue)
{
    bool ret = false;
    
    for(int count = 0; count < 100; ++count)
    {
        while(false == queue.insert(count));            
    }
    
    return;
}

template<typename T>
void consumer(Queue<T>& queue)
{
    int val = 0;
    while(true)
    {
        queue.get(&val);
        
        cout << "Val popped : " << val << endl;
        
        if(val == 99) break;
    }
    
    return;
}

template<typename T>
class thread_guard
{
private:
    void (*thread_func)(Queue<T> &);
    Queue<T>& q_ref_;
    std::thread this_thread;
public:
    thread_guard(void (*thread_function)(Queue<T> &), Queue<T>& q_ref):
        thread_func{thread_function}, q_ref_{q_ref}, 
        this_thread(thread_func, std::ref(q_ref_))
    {
        cout << "Thread guard crated" << endl;
    }
    
    ~thread_guard()
    {
        this_thread.join();
        cout << "Thread has joined back and thread_guard is destroyed" << endl;
    }
    
    // delete any copy/move operations
    thread_guard(const thread_guard&) =delete;
    thread_guard(thread_guard&&) =delete;
    thread_guard& operator=(const thread_guard&) =delete;
    thread_guard& operator=(thread_guard&&) =delete;
};

Let us see the main() now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main()
{    
    Queue<int> q1(10);
    
    // create consumer thread first 
    thread_guard<int> th_g_consumer(consumer<int>, q1);
    
    // create producer thread
    thread_guard<int> th_g_producer(producer<int>, q1);
    
    cout << "Main ends" << endl;
    
    return 0;
}

See now, main does not have to bother about joining the threads and it ends right after creating the thread_guards for producer and consumer. You can see “Main Ends” is printed in result-line #4 itself, but the threads executed till their completion and joined back the thread_guards in their destructors.

Programmer needs to use thread_guards to manage a thread’s life-cycle efficiently adhering to the principles of RAII.

Result:

Queue with size : 10 is created
Thread guard crated
Thread guard crated
        Queue is full : 10 Main ends
Val popped : 0
        Queue is full : 11
Val popped : 1
        Queue is full : 12
Val popped : 2
        Queue is full : 13
Val popped : 3
        Queue is full : 14
Val popped : 4
        Queue is full : 15
Val popped : 5
        Queue is full : 16
Val popped : 6
        Queue is full : 17
Val popped : 7
        Queue is full : 18
Val popped : 8
        Queue is full : 19
Val popped : 9
        Queue is full : 20
Val popped : 10
        Queue is full : 21
Val popped : 11
        Queue is full : 22
Val popped : 12
        Queue is full : 23
Val popped : 13
        Queue is full : 24
Val popped : 14
        Queue is full : 25
Val popped : 15
        Queue is full : 26
Val popped : 16
        Queue is full : 27
Val popped : 17
        Queue is full : 28
Val popped : 18
        Queue is full : 29
Val popped : 19
        Queue is full : 30
Val popped : 20
        Queue is full : 31
Val popped : 21
        Queue is full : 32
Val popped : 22
        Queue is full : 33
Val popped : 23
        Queue is full : 34
Val popped : 24
        Queue is full : 35
Val popped : 25
        Queue is full : 36
Val popped : 26
        Queue is full : 37
Val popped : 27
        Queue is full : 38
Val popped : 28
        Queue is full : 39
Val popped : 29
        Queue is full : 40
Val popped : 30
        Queue is full : 41
Val popped : 31
        Queue is full : 42
Val popped : 32
        Queue is full : 43
Val popped : 33
        Queue is full : 44
Val popped : 34
        Queue is full : 45
Val popped : 35
        Queue is full : 46
Val popped : 36
        Queue is full : 47
Val popped : 37
        Queue is full : 48
Val popped : 38
        Queue is full : 49
Val popped : 39
        Queue is full : 50
Val popped : 40
        Queue is full : 51
Val popped : 41
        Queue is full : 52
Val popped : 42
        Queue is full : 53
Val popped :    Queue is full : 4354
Val popped : 44
        Queue is full : 55
Val popped : 45
        Queue is full : 56
Val popped : 46
        Queue is full : 57
Val popped : 47
        Queue is full : 58
Val popped : 48
        Queue is full : 59
Val popped : 49
        Queue is full : 60
Val popped : 50
        Queue is full : 61
Val popped : 51
        Queue is full : 62
Val popped : 52
        Queue is full : 63
Val popped : 53
        Queue is full : 64
Val popped : 54 Queue is full : 65
        Queue is full : 65
Val popped : 55
        Queue is full : 66
Val popped : 56
        Queue is full : 67
Val popped : 57
        Queue is full : 68
Val popped :    Queue is full : 69
58
        Queue is full : 69
Val popped : 59
        Queue is full : 70
Val popped : 60
        Queue is full : 71
Val popped : 61
        Queue is full : 72
Val popped : 62
        Queue is full : 73
Val popped : 63
        Queue is full : 74
Val popped : 64
        Queue is full : 75
Val popped : 65
        Queue is full : 76
Val popped : 66
        Queue is full : 77
Val popped : 67
        Queue is full : 78
Val popped : 68 Queue is full :
79
Val popped : 69
        Queue is full : 80
Val popped : 70
        Queue is full : 81
Val popped : 71
        Queue is full : 82
Val popped : 72
        Queue is full : 83
Val popped : 73
        Queue is full : 84
Val popped : 74
        Queue is full : 85
Val popped : 75
        Queue is full : 86
Val popped : 76
        Queue is full : 87
Val popped : 77
        Queue is full : 88
Val popped : 78
        Queue is full : 89
Val popped : 79
        Queue is full : 90
Val popped : 80
        Queue is full : 91
Val popped : 81
        Queue is full : 92
Val popped : 82
        Queue is full : 93
Val popped : 83
        Queue is full : 94
Val popped : 84
        Queue is full : 95
Val popped : 85
        Queue is full : 96
Val popped : 86
        Queue is full : 97
Val popped : 87
        Queue is full : 98
Val popped : 88
        Queue is full : 99
Val popped : 89
Thread has joined back and thread_guard is destroyed
Val popped : 90
Val popped : 91
Val popped : 92
Val popped : 93
Val popped : 94
Val popped : 95
Val popped : 96
Val popped : 97
Val popped : 98
Val popped : 99
Thread has joined back and thread_guard is destroyed
Queue with size : 10 is destroyed

Observe how the thread_guard made sure the threads are joined back after completing their functions.

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

0

Chapter 10 : Shared Pointers, An Introduction To Atomics In C++

Key words: Shared Pointers in C++, Implementing a custom shared_ptr in C++, Atomics in C++

Topics at a glance:

  • Sharing ownership via Shared Pointers and their need
  • Atomics in Shared pointer

In this chapter, I will show you how to make a custom Shared_Ptr from our Chapter 9’s Unique_Ptr using C++’s atomics.

Shared Pointers in C++

Shared Pointers using template specialization and C++ atomics

We have seen how to implement our own Unique_Ptr class using template specialization. Unique_Ptr’s only support move semantics and no copy operation is allowed. Of course the sole purpose of Unique_Ptr is to avoid multiple owners for the same memory region at any given point of time. Using move you can only transfer ownership of memory through a Unique_Ptr. What if you want shared ownership? Then we need to use C++’s 2nd smart pointer i.e. the ‘std::shared_ptr’. Shared pointers allow multiple ownership achieved through two things:

  1. Shared pointers allow copy semantics (obviously)
  2. Shared pointers uses a reference count (use count) mechanism that tracks the current number of owners that uses the shared memory region
  3. Shared pointers will delete/release the memory back to free store only if the reference count reaches 0.

So, that is how shared pointers work.

Implementing a custom shared_ptr in C++

Now, to implement our shared pointer, let us use the Unique_Ptr code from previous chapter and rename ‘Unique_Ptr’ to ‘Shared_Ptr’. Not only that. We will need to introduce a reference counter. Reference counter cannot be a local count data to Shared_Ptr object instances as after copying the ownership Shared_Ptr should use the exact same instance of reference count to track the owners in all copies of Shared_Ptr objects.

This bring another issue. Unlike old generation computers and OS’s all modern CPU’s support actual multi-threading and parallel computing. So there can be multiple threads sharing ownership of memory through instances of Shared_Ptr’s. This will result in data race condition. And reference count is likely to be changed from different threads simultaneously. Reference count is the only thing that matters for Shared_Ptrs to delete pointer and release back the memory. So it is imperative to protect reference count from data race conditions associated with multi-threaded environments.  How to do that?

Atomics in C++

C++ gives some essential facilities to deal with issues inherent to multi-threaded environments. One we can consider here is mutex lock (mutually exclusive lock), and other is atomics. I prefer atomics here. Mutex is very difficult to apply here. As who will take care of releasing the mutex itself at the end of Shared Pointer’s life-cycle. The same mutex needs to be used for the same reference count in case of shared ownership. So why to go for all such headaches. Let us use the simple atomics. Atomics guarantees completion of execution of any operation without interruption. We’ll use C++’s std::atomic for defining the reference counter in our Shared_Ptr. So I will point out once again the key features to be implemented on Unique_Ptr to make it a Shared_Ptr class.

From Unique_Ptr to Shared_Ptr code modification:

  1. Replace Unique_Ptr to Shared_Ptr
  2. Define copy operations
  3. Implement an atomic reference count member that could be shared between copies of Shared_Ptr instances
  4. Define a use_count() member function that will return the current value of reference count

That’s it. Now, with all this information, let us implement Shared_Ptr. NOTE: For now, I am not defining any custom make_shared as we did for Unique_Ptrs. Let us use the basic new operation for constructing objects and pass the returned pointers to make Shared_Ptr instances.

Also note that I have retained all the default and custom deleters that I have used in Unique_Ptr.

Please go through the detailed and elaborated code below, and understand how the points discussed above are actually implemented.

  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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
#include <iostream>
#include <atomic>

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 Shared_Ptr
{
private:
    std::atomic<int*> reference_count;
    T* raw_ptr;
    TP deleter; // will get constructed anyways with default_deleter type  
public:
    explicit Shared_Ptr(T* object) : 
        raw_ptr{object}, 
        reference_count(new int(1)) // cannot use {}, use ()
    {
        cout << "Shared_Ptr created with Ref count : " << *reference_count << endl;
    }
    
    // constructor which accepts a custom deleter 
    Shared_Ptr(T* object, TP custom_deleter) : 
        raw_ptr{object}, 
        deleter{custom_deleter}, 
        reference_count(new int(1)) // cannot use {}, use ()
    {
        cout << "Shared_Ptr created with Ref count : " << *reference_count << endl;
    }
    
    ~Shared_Ptr()
    {
        if(reference_count != nullptr)
        {
            --(*reference_count);
            cout << "In Shared_Ptr Destructor Ref count : " << *reference_count << endl;
            if(*reference_count <= 0)
            {
                if(raw_ptr != nullptr)
                {
                    // call deleter
                    deleter(raw_ptr);
                }
            }
        }
    }
    
    TP& get_deleter()
    {
        return deleter;
    }
    
    // define copy operations
    Shared_Ptr(const Shared_Ptr& other_shared_ptr)
    {
        // do not create a new reference_count, copy the pointer from other_shared_ptr
        reference_count = other_shared_ptr.reference_count.load();
        if(reference_count != nullptr)
        {
            ++(*reference_count);
            cout << "In Shared_Ptr Copy Constructor Ref count : " << *reference_count << endl;
        }
        raw_ptr = other_shared_ptr.raw_ptr;
        deleter = other_shared_ptr.deleter;
    }
    
    Shared_Ptr& operator=(const Shared_Ptr& other_shared_ptr)
    {
        // do not create a new reference_count, copy the pointer from other_shared_ptr
        reference_count = other_shared_ptr.reference_count.load();
        if(reference_count != nullptr)
        {
            ++(*reference_count);
            cout << "In Shared_Ptr Copy =operator Ref count : " << *reference_count << endl;
        }
        raw_ptr = other_shared_ptr.raw_ptr;
        deleter = other_shared_ptr.deleter;
        
        return *this;
    }
    
    // Define move operations
    Shared_Ptr(Shared_Ptr&& other_shared_ptr)
    {
        // transfer the ownership of memory 
        raw_ptr = other_shared_ptr.raw_ptr;
        other_shared_ptr.raw_ptr = nullptr;
        reference_count = other_shared_ptr.reference_count.load();
        other_shared_ptr.reference_count = nullptr;
        
        deleter = other_shared_ptr.deleter;        
    }
    
    Shared_Ptr& operator=(Shared_Ptr&& other_shared_ptr)
    {
        // transfer the ownership of memory 
        raw_ptr = other_shared_ptr.raw_ptr;
        other_shared_ptr.raw_ptr = nullptr;
        reference_count = other_shared_ptr.reference_count.load();
        other_shared_ptr.reference_count = nullptr;
        
        deleter = other_shared_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 Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr == other_shared_ptr.raw_ptr);
    }
    
    // 5. in-equality check 
    bool operator!=(const Shared_Ptr& other_shared_ptr) const 
    {
        return!(this->operator==(other_shared_ptr));
    }
    
    // 6. less than 
    bool operator<(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr < other_shared_ptr.raw_ptr);
    }
    
    // 7. greater than 
    bool operator>(const Shared_Ptr& other_shared_ptr) const 
    {
        return!(this->operator<(other_shared_ptr));
    }    
    
    // 8. less than or equal  
    bool operator<=(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr <= other_shared_ptr.raw_ptr);
    }
    
    // 9. greater than or equal 
    bool operator>=(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr >= other_shared_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;
        reference_count = nullptr;
        
        return temp;
    }
    
    void reset(T* new_ptr)
    {
        T* old_ptr =  raw_ptr;
        raw_ptr = new_ptr;
        reference_count = 1; // reset back to 1
        
        if(old_ptr != nullptr)
        {
            deleter(old_ptr); 
        }
    }
    
    int use_count() const 
    {
        return *reference_count;
    }
    
};

// Array version T[] with custom deleter support 
template< typename T, typename TP > // Here TP already has the default type as default_deleter<T>
class Shared_Ptr<T[], TP> // Must have Shared_Ptr<T> already defined for T[] to work 
{
private:
    std::atomic<int*> reference_count;
    T * raw_ptr; // pointer to an array of T's
    TP deleter;
public:
    Shared_Ptr(T *object) : 
        raw_ptr{object}, 
        deleter{default_deleter<T[]>()}, // Here we copy-construct specialized deleter<T[]>
        reference_count(new int(1)) // cannot use {}, use ()
    {
        cout << "Shared_Ptr [] created with Ref count : " << *reference_count << endl;
    }
    
    Shared_Ptr(T* object, TP this_deleter): 
        raw_ptr{object}, deleter{this_deleter}, 
        reference_count(new int(1)) // cannot use {}, use ()
    {
        cout << "Shared_Ptr [] created with Ref count : " << *reference_count << endl;
    }
    
    ~Shared_Ptr()
    {
        if(reference_count != nullptr)
        {
            --(*reference_count);
            cout << "Shared_Ptr [] destructor Ref count : " << *reference_count << endl;
            if(*reference_count == 0)
            {
                if(raw_ptr != nullptr)
                {
                    deleter(raw_ptr);
                }
            }
        }
    }
    
    TP& get_deleter()
    {
        return deleter;
    }
    
    // define copy operations
    Shared_Ptr(const Shared_Ptr& other_shared_ptr)
    {
        reference_count = other_shared_ptr.reference_count.load();
        raw_ptr = other_shared_ptr.raw_ptr;
        
        if(raw_ptr != nullptr)
        {
            ++(*reference_count);
        }
        
        deleter = other_shared_ptr.deleter;
        
        cout << "In Shared_Ptr [] Copy constructor Ref count : " << *reference_count << endl;
        
    }
    
    Shared_Ptr& operator=(const Shared_Ptr& other_shared_ptr)
    {
        reference_count = other_shared_ptr.reference_count.load();
        raw_ptr = other_shared_ptr.raw_ptr;
        
        if(raw_ptr != nullptr)
        {
            ++(*reference_count);
        }
        
        deleter = other_shared_ptr.deleter;
        
        cout << "In Shared_Ptr [] Copy =operator Ref count : " << *reference_count << endl;
        
        return *this;
    }
    
    // Define move operations
    Shared_Ptr(Shared_Ptr&& other_shared_ptr)
    {
        // transfer the ownership of memory 
        raw_ptr = other_shared_ptr.raw_ptr;
        other_shared_ptr.raw_ptr = nullptr;
        
        deleter = other_shared_ptr.deleter; 
        
        reference_count = other_shared_ptr.reference_count.load();
        other_shared_ptr.reference_count = nullptr;
        
    }
    
    Shared_Ptr& operator=(Shared_Ptr&& other_shared_ptr)
    {
        // transfer the ownership of memory 
        raw_ptr = other_shared_ptr.raw_ptr;
        other_shared_ptr.raw_ptr = nullptr;
        
        deleter = other_shared_ptr.deleter; 
        
        reference_count = other_shared_ptr.reference_count.load();
        other_shared_ptr.reference_count = 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 Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr == other_shared_ptr.raw_ptr);
    }
    
    // 5. in-equality check 
    bool operator!=(const Shared_Ptr& other_shared_ptr) const 
    {
        return!(this->operator==(other_shared_ptr));
    }
    
    // 6. less than 
    bool operator<(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr < other_shared_ptr.raw_ptr);
    }
    
    // 7. greater than 
    bool operator>(const Shared_Ptr& other_shared_ptr) const 
    {
        return!(this->operator<(other_shared_ptr));
    }    
    
    // 8. less than or equal  
    bool operator<=(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr <= other_shared_ptr.raw_ptr);
    }
    
    // 9. greater than or equal 
    bool operator>=(const Shared_Ptr& other_shared_ptr) const 
    {
        return(raw_ptr >= other_shared_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;
        reference_count = nullptr;
        
        return temp;
    }
    
    void reset(T* new_ptr)
    {
        T* old_ptr =  raw_ptr;
        raw_ptr = new_ptr;
        reference_count = 1; // reset back to 1
        
        if(old_ptr != nullptr)
        {
            deleter(old_ptr);
        }
    }
    
    int use_count() const 
    {
        return *reference_count;
    }
    
};

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

int main()
{   
    {
        Shared_Ptr<int> sh_ptr1(new int); 
        // local scope 1
        {
            Shared_Ptr<int> sh_ptr2 = sh_ptr1; // copy construction 
            *sh_ptr2 = 5;
        }
        
        cout << "Outside local scope 1" << endl;
        cout << "*shptr1 : " << *sh_ptr1 << endl;
    
    }
    
    cout << endl;
    
    {
        const int size = 10;
        Shared_Ptr<int[]> sh_ptr3(new int[size]); 
        // local scope 2
        {
            Shared_Ptr<int[]> sh_ptr4 = sh_ptr3; // copy construction 
            for(int index = 0; index < size; ++index)
            {
                sh_ptr4[index] = index*10;
            }
        }
        
        cout << "Outside local scope 2" << endl;
        
        for(int index = 0; index < size; ++index)
        {
            cout << "sh_ptr3[" << index << "] : " << sh_ptr3[index] << endl;
        }
    }
    
    return 0;
}

I will explain later why we have to use normal parantheses () instead of curly braces {}, way of construction for atomic reference count, also, why to use load () while copying atomic variables. For now, just focus on the working of shared pointers.

Let us see how reference count actually tracks the shared ownership. I have purposefully defined local scopes in the main() to show how reference count is checked by the Shared_Ptr’s destructor before deleting the pointer and releasing the memory.

Let us see the result now:

Shared_Ptr created with Ref count : 1
In Shared_Ptr Copy Constructor Ref count : 2
In Shared_Ptr Destructor Ref count : 1
Outside local scope 1
*shptr1 : 5
In Shared_Ptr Destructor Ref count : 0
default_deleter
Shared_Ptr [] created with Ref count : 1
In Shared_Ptr [] Copy constructor Ref count : 2
Shared_Ptr [] destructor Ref count : 1
Outside local scope 2
sh_ptr3[0] : 0
sh_ptr3[1] : 10
sh_ptr3[2] : 20
sh_ptr3[3] : 30
sh_ptr3[4] : 40
sh_ptr3[5] : 50
sh_ptr3[6] : 60
sh_ptr3[7] : 70
sh_ptr3[8] : 80
sh_ptr3[9] : 90
Shared_Ptr [] destructor Ref count : 0
default_deleter []

Try to track the reference count of shared Shared_Ptr instances and see when the actual delete is getting called.

The thread safety of shared pointers are to be discussed further. The standard only guarantees atomic operations on the reference count of the shared pointer and not on the object itself that shared pointer points to. I have only made the reference count as atomic here.

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

0

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

0

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

Chapter 7 : The Philosophy Of C++, Part C

Key words: copy derived class objects, cloning in C++, moving derived class objects

Topics at a glance:

  • Introduction to cloning idiom in C++
  • Derived class objects and the copy conundrum
  • How to clone correctly ?
  • Moving objects of derived classes

Derived Class Objects : The Copy Conundrum!

When an object of a derived class has to be copied, then we cannot go with the usual approach of copy constructors. Because what if the client code who is going to initiate the copy operation is adhering to our code to base class object principle. In that case, we cannot resort to virtual constructors.

Because in C++ there is no virtual constructor, unlike virtual destructors. So in that case if the actual object to clone from is a type of derived class, then client code will end up calling just the base class constructor and not the derived class constructor because of lack of polymorphic behavior for constructors in C++. You get the issue here? What you asked is to clone from a derived class instance, and what you got is just a copy of a base class instance. i.e. object is only partly copied. This problem is known as ‘object slicing‘ in C++. So the question now is how to avoid this object slicing’ from happening?

Copy derived class objects

Cloning in C++

  1. Make base class copy constructor ‘protected’ so that only derived classes will have access privilege to base class copy constructors.
  2. Make derived class copy constructor ‘private’ so that it is not accessible outside the class.
  3. Define a virtual clone() member function for your base class and override this in derive class.

Now, let us see the above steps in action. I have written a code for cloning a derived class object. The client code here is the main() function itself, and sticks to the code to a base class object  principle.

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

using namespace std;

class Base
{
private:
    int a_;
public:
    Base(const int a = 0) : a_{a}
    {
        cout << "Base created with a_ : " << a_ << endl;
    }
    
    virtual ~Base()
    {
        cout << "Base object destroyed" << endl;
    }
protected: // MUST be protected so that this wont be available outside base/child classes 
    // copy constructor 
    Base(const Base& other_object)
    {
        a_ = other_object.a_;
        cout << "Base created with a_ : " << a_ << endl;
    }
public:
    virtual Base* clone() 
    {
        cout << "Cloning Base object..." << endl;
        return new Base(*this); // invokes Base copy constructor 
    }
    
    virtual void print()
    {
        cout << "a_ : " << a_ << endl; 
    }
};

class Derived : public Base
{
private:
    int b_;
    int c_;
public:
    Derived(const int a = 0, const int b = 0, const int c = 0): Base(a), b_{b}, c_{c}
    {
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
    ~Derived()
    {
        cout << "Derived object destroyed" << endl;
    }
private:
    // copy constructor is made private 
    Derived(const Derived& other_object) : Base(other_object)
    {
        b_ = other_object.b_;
        c_ = other_object.c_;
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
public:
    Base* clone() override
    {
        cout << "cloning Derived object..." << endl;
        return new Derived(*this);
    }
    
    void print() override
    {
        // First print Base 
        Base::print();
        cout << "b_ : " << b_ << "\nc_ : " << c_ << endl;
    }
    
};

int main()
{
    Derived d1(1, 2, 3);
    
    Base *b1 = d1.clone();
    
    b1->print();    
    
    cout << "\nCreating a second clone of Derived..." << endl;
    Base *b2 = b1->clone();
    
    b2->print();
    
    // Destroy all cloned objects 
    cout << "\nDestroying cloned objects..." << endl;
    
    delete b1;
    delete b2;
    
    cout << "Cloned objects destroyed" << endl;
    
    cout << "\nAt the end of main() Object 'd1' in main's stack will also be destroyed!" << endl;
    
    return 0;
}

Now, let us see the result of this code when executed:

Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
Creating a second clone of Derived...
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
Destroying cloned objects...
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Cloned objects destroyed
At the end of main() Object 'd1' in main's stack will also be destroyed!
Derived object destroyed
Base object destroyed

It works fine, but with a hidden major issue with the implementation. The issue is, client code unknowingly takes up the ownership of the cloned objects. The clone objects are actually being constructed in the free store by the clone() function and the function give away the ownership to client. So it is client responsibility to release the memory properly after using it. In this case, I know that main() is going to end and exit. But in real programs client code won’t be a main and it won’t exit the program right after cloning and using the objects and most likely client code returns without cleaning up the cloned objects and memory is never released back to free store. The issue will become worse if the objects have some other resources that are acquired inside, like files, sockets etc. So what would have happened if cloned objects are not destroyed properly? To avoid such issues, client code has to delete the cloned objects and clean the mess up just before returning. But as I have said in the session above, the principle of RAII just asks the programmers to restrain from using naked new and delete. In this case, ‘new’ is not naked as it is hidden inside the clone() method, but since the clone() has given away the ownership of the object, now its client’s responsibility to directly call delete; and moreover a naked delete, violating RAII.

So, to avoid this, make use of modern C++ ‘s smart pointers. Let us see the steps below:

  1. Make use of unique pointers instead of raw pointers thus avoiding any issues related to ownership and life cyle management of objects.
  2. In the code here, Base class’s copy constructor is protected and Derived class’s copy constructor is private. From C++ 14 onward, std::unique_ptr has to be constructed using a factory method ‘std::make_unique’. This method is an outsider for the Base and Derived class and therefore it’s not possible to manipulate creation of Base/Derived objects via the private/protected copy constructors.
  3. To circumvent the issue mentioned in step 2, one solution is to use the C++’s powerful friend function. (I agree long-distance friendships are not recommended, but for now let’s go with it)

So, with all these in our armoury let us resolve this issue:

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

using namespace std;

class Base
{
private:
    int a_;
public:
    Base(const int a = 0) : a_{a}
    {
        cout << "Base created with a_ : " << a_ << endl;
    }
    
    virtual ~Base()
    {
        cout << "Base object destroyed" << endl;
    }
protected: // MUST be protected so that this wont be available outside base/child classes 
    // copy constructor 
    Base(const Base& other_object)
    {
        a_ = other_object.a_;
        cout << "Base created with a_ : " << a_ << endl;
    }
public:
    virtual unique_ptr<Base> clone() 
    {
        cout << "Cloning Base object..." << endl;
        return make_unique<Base>(*this);
    }
    
    virtual void print()
    {
        cout << "a_ : " << a_ << endl; 
    }
    
    friend unique_ptr<Base> make_unique<Base>(Base&);
};

class Derived : public Base
{
private:
    int b_;
    int c_;
public:
    Derived(const int a = 0, const int b = 0, const int c = 0): Base(a), b_{b}, c_{c}
    {
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
    ~Derived()
    {
        cout << "Derived object destroyed" << endl;
    }
private:
    // copy constructor is made private 
    Derived(const Derived& other_object) : Base(other_object)
    {
        b_ = other_object.b_;
        c_ = other_object.c_;
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
public:
    unique_ptr<Base> clone() override
    {
        cout << "cloning Derived object..." << endl;
        return make_unique<Derived>(*this);
    }
    
    void print() override
    {
        // First print Base 
        Base::print();
        cout << "b_ : " << b_ << "\nc_ : " << c_ << endl;
    }
    
    friend unique_ptr<Derived> make_unique<Derived>(Derived&);
};

int main()
{
    Derived d1(1, 2, 3);
    
    unique_ptr<Base> b1 = d1.clone();
    
    b1->print();    
    
    cout << "\nCreating a second clone of Derived..." << endl;
    unique_ptr<Base> b2 = b1->clone();
    
    b2->print();
    
    cout << "\nAt the end of main() Object 'd1' in main's stack,"
         << "\nalong with cloned objects will be destroyed!" << endl;
    
    return 0;
}

The main thing to note here is that in the client code i.e. main(), there is no need to call delete anymore. ‘unique_ptr’ takes care of this automatically. Now, let’s see the result.

Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
Creating a second clone of Derived...
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
At the end of main() Object 'd1' in main's stack,
along with cloned objects will be destroyed!
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed

Let us summarize the things we’ve done for making a copy of derived class object.

  1. Cannot use C++ copy constructor directly for derived class object copy
  2. Define virtual clone() method in Base and override that in Derived class
  3. These clone() methods uses the protected/private copy constructors for creating clones.
  4. clone() gives away the ownership of the created object as clone() is just a factory method here.
  5. Client code has to clean-up the cloned objects after their use in the code or before returning leading to naked ‘delete’.
  6. To resolve issue in step ‘5’ make use of C++ std::unique_ptr
  7. To create std::unique_ptr the C++ 14 way, we have to use std::make_unique.
  8. Make the outsider function, std::make_unique a friend of Base and Derived class.
  9. Voila! 😊

Moving derived class objects

Now, as an extension of the clone idiom, let us develop the rest of the copy/move operations such as

  1. Move construction
  2. Copy assignment
  3. Move assignment

Move constructor:

Do you remember that the main reason for clone() method for copy construction was that there is no virtual constructor in C++. The same is applicable for move construction as well. So, for move construction also, we have to come up with a polymorphic method similar to clone. Here, I am defining polymorphic move() methods for Base and Derived classes just like their clone() counterparts.

i.e. For Base class move will be virtual, which will get overridden by Derived class’s move. Rest of the things are similar to the behavior of regular move construction operation.

One important thing to note in move construction is that we have to declare std::make_unique as a friend function of both Base and Derived class to take r-value references of Base and Derived. i.e.

‘friend unique_ptr<Base> std::make_unique<Base>(Base&&);’

‘friend unique_ptr< Derived > std::make_unique< Derived >(Derived&&);’

Copy assignment:

Unlike its constructor counterpart, here we can make use of assignment ‘operator=’ for Base-Derived class hierarchy.

There is no need of an explicit polymorphic method for assignment operations. Just define the usual copy assignment variant of ‘operator=’ for both base and derived classes. But make sure you call the base variant of “operator=’ from derived’s ‘operator=’ and this must be the first thing to do i.e to copy base class object data first before derived class object data. This call chaining of ‘operator=’, in an ordered manner, is required as you cannot define virtual copy/move assignments as class names will not match between base and derived, thus making the function signatures differ between base and derived variants of ‘operator=’.

Move assignment:

Unlike its constructor counterpart in move assignment also, we don’t need another polymorphic method. Define the move assignment variant of ‘operator=’ for both base and derived, and do a similar call chaining I have mentioned in copy assignment above. i.e. to call base’s ‘operator=’ variant before derived’s variant.

The important things to note for copy/move ‘operator=’ are:

  1. You cannot make virtual copy/move assignment operators as the function signatures must match. For base class it uses ‘const Base&‘ or ‘Base&&‘ for copy and move assignment ‘operator=’ respectively, whereas for derived class it uses ‘const Derived&‘ or ‘Derived&&‘ for copy and move ‘operator=’ respectively. For polymorphic behavior function signatures must match exactly.
  2. You have to call-chain. i.e derived’s variant of ‘operator=’ must invoke the base’s variant and only then both base and derived object data will get copied/moved.
  3. The order of call-chain is so important. i.e base variant of ‘operator=’ must be called first to copy/move base object’s data before derived’s. Because in some classes, derived will inherit base’s protected and public data members and may even overwrite these values. So if base class copy/move is done after derived, then it’s likely to overwrite the derived object’s data with base object data.

Let us put this information into action now:

The below code implements copy assignment, move constructor and move assignment operation of Base-Derived class system along with the copy construction’s clone as per the above mentioned ways.

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

using namespace std;

class Base
{
private:
    int a_;
public:
    Base(const int a = 0) : a_{a}
    {
        cout << "Base created with a_ : " << a_ << endl;
    }
    
    virtual ~Base()
    {
        cout << "Base object destroyed" << endl;
    }
protected: // MUST be protected so that this wont be available outside base/child classes 
    // copy constructor 
    Base(const Base& other_object)
    {
        a_ = other_object.a_;
        cout << "Base created with a_ : " << a_ << endl;
    }
    // Base class move constructor
    Base(Base&& other_object)
    {
        a_ = other_object.a_;
        other_object.a_ = 0;
        cout << "Base object has been move-constructed with a_ : " << a_ << endl;
    }
public:
    // copy assignment
    Base& operator=(const Base& other_object)
    {
        a_ = other_object.a_;
        cout << "Base object has been copy assigned with a_ : " << a_ << endl;
        return (*this);
    }
    
    // move assignment
    Base& operator=(Base&& other_object)
    {
        a_ = other_object.a_;
        other_object.a_ = 0;
        
        cout << "Base object has been move assigned with a_ : " << a_ << endl;
        
        return (*this);
    }
    
    virtual unique_ptr<Base> clone() 
    {
        cout << "Cloning Base object..." << endl;
        return make_unique<Base>(*this);
    }
    
    virtual unique_ptr<Base> move()
    {
        cout << "Moving Base object..." << endl;
        return make_unique<Base>(std::move(*this));
    }
    
    virtual void print()
    {
        cout << "a_ : " << a_ << endl; 
    }
    
    friend unique_ptr<Base> make_unique<Base>(Base&);
    friend unique_ptr<Base> make_unique<Base>(Base&&);
};

class Derived : public Base
{
private:
    int b_;
    int c_;
public:
    Derived(const int a = 0, const int b = 0, const int c = 0): Base(a), b_{b}, c_{c}
    {
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
    ~Derived()
    {
        cout << "Derived object destroyed" << endl;
    }
private:
    // copy constructor is made private 
    Derived(const Derived& other_object) : Base(other_object)
    {
        b_ = other_object.b_;
        c_ = other_object.c_;
        cout << "Derived created with b_ : " << b_ << " c_ : " << c_ << endl;
    }
    // move constructor
    Derived(Derived&& other_object): Base(std::move(other_object)) 
    {
        b_ = other_object.b_;
        c_ = other_object.c_;
        
        other_object.b_ = 0;
        other_object.c_ = 0;
        
        cout << "Derived object has been move-constructed with b_ : " << b_ 
             << " c_ : " << c_ << endl;
    }
public:
    // copy assignment
    Derived& operator=(const Derived& other_object)
    {
        // first assign base class object
        Base::operator=(static_cast<const Base&>(other_object)); 
        // no need to take the returned value as it is the same 'this' object
        
        b_ = other_object.b_;
        c_ = other_object.c_;
        cout << "Derived object has been copy assigned with b_ : " << b_ 
             << " c_ : " << c_ << endl;
             
        return (*this);
    }
    
    // move assignment
    Derived& operator=(Derived&& other_object)
    {
        // first assign base class object
        Base::operator=(static_cast<Base&&>(std::move(other_object))); 
        // no need to take the returned value as it is the same 'this' object
        
        b_ = other_object.b_;
        c_ = other_object.c_;
        
        other_object.b_ = 0;
        other_object.c_ = 0;
        
        cout << "Derived object has been move assigned with b_ : " << b_ 
             << " c_ : " << c_ << endl;
             
        return (*this);
    }
    unique_ptr<Base> clone() override
    {
        cout << "cloning Derived object..." << endl;
        return make_unique<Derived>(*this);
    }
    
    unique_ptr<Base> move() override
    {
        cout << "Moving Derived object..." << endl;
        return make_unique<Derived>(std::move(*this));
    }
    
    void print() override
    {
        // First print Base 
        Base::print();
        cout << "b_ : " << b_ << "\nc_ : " << c_ << endl;
    }
    
    friend unique_ptr<Derived> make_unique<Derived>(Derived&);
    friend unique_ptr<Derived> make_unique<Derived>(Derived&&);
};

void test_move_assignment()
{
    cout << "\nTesting move assignment..." << endl;
    
    Derived d5(10, 11, 12);
    
    Derived d6;
    
    d6 = std::move(d5);
    
    d6.print();
    
    cout << "Testing move assignment done\n" << endl;
    
    return;    
}

void test_copy_assignment()
{
    cout << "\nTesting copy assignment..." << endl;
    
    Derived d3(7, 8, 9);
    
    Derived d4;
    
    d4 = d3;
    
    d4.print();
    
    cout << "Testing copy assignment done\n" << endl;
    
    return;
}

void test_move_semantics()
{
    cout << "\nTesting move semantics..." << endl;
    
    Derived d2(4, 5, 6);
    
    unique_ptr<Base> b3 = d2.move();
    
    b3->print();
    
    cout << "Testing move semantics done!\n" << endl;
    
    return;
}

void test_clone()
{
    Derived d1(1, 2, 3);
    
    unique_ptr<Base> b1 = d1.clone();
    
    b1->print();    
    
    cout << "\nCreating a second clone of Derived..." << endl;
    unique_ptr<Base> b2 = b1->clone();
    
    b2->print();
    
    cout << "\nAt the end of test_clone() Object 'd1' in test_clone's stack,"
         << "\nalong with cloned objects will be destroyed!" << endl;
         
    return;
    
}

int main()
{
    test_clone();
    
    test_move_semantics();
    
    test_copy_assignment();
    
    test_move_assignment();    
    
    return 0;
}

Now, let us see the results:

Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
Creating a second clone of Derived...
cloning Derived object...
Base created with a_ : 1
Derived created with b_ : 2 c_ : 3
a_ : 1
b_ : 2
c_ : 3
At the end of test_clone() Object 'd1' in test_clone's stack,
along with cloned objects will be destroyed!
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Testing move semantics...
Base created with a_ : 4
Derived created with b_ : 5 c_ : 6
Moving Derived object...
Base object has been move-constructed with a_ : 4
Derived object has been move-constructed with b_ : 5 c_ : 6
a_ : 4
b_ : 5
c_ : 6
Testing move semantics done!
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Testing copy assignment...
Base created with a_ : 7
Derived created with b_ : 8 c_ : 9
Base created with a_ : 0
Derived created with b_ : 0 c_ : 0
Base object has been copy assigned with a_ : 7
Derived object has been copy assigned with b_ : 8 c_ : 9
a_ : 7
b_ : 8
c_ : 9
Testing copy assignment done
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed
Testing move assignment...
Base created with a_ : 10
Derived created with b_ : 11 c_ : 12
Base created with a_ : 0
Derived created with b_ : 0 c_ : 0
Base object has been copy assigned with a_ : 10
Derived object has been move assigned with b_ : 11 c_ : 12
a_ : 10
b_ : 11
c_ : 12
Testing move assignment done
Derived object destroyed
Base object destroyed
Derived object destroyed
Base object destroyed.

I hope you have understood the copy and move semantics to be applied for a Base-Derived class hierarchy now.

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

0

Chapter 6 : The Philosophy Of C++, Part B

Key words: Object management in C++, constructors, destructors, object’s life-cycle, the principle of Resource Acquisition Is Initialization (RAII), the fundamentals of copy and move semantics in modern C++, copy constructor, copy assignment operator, shallow copy vs deep copy, compiler default copy , r-value and r-value reference in modern C++, move constructor, move assignment operator

Topics at a glance

  • Introduction to object management
  • The beauty of creation with constructors
  • The horrors of wandering : Resource leakage, destructors to the rescue
  • Virtual destructors and their significance
  • The principle of Resource Acquisition Is Initialization (RAII)
  • Establishing class invariants
  • Making copies via copy constructors and copy assignment operators
  • The dangers of shallow copy and understanding deep copy
  • Moving objects via move constructors and move assignment operators
  • Compiler defaults, their vagaries in copying and moving objects

Object Management in C++

Here, I want to show you the C++’s elegant mechanisms for object management. First let us see how an object’s life-cycle management is done through constructors and destructors, and then, how objects can be copied and moved around using modern C++’s copy and move semantics. Then we’ll see the famous C++ idiom Resource Acquisition Is Initialization abbreviated as RAII.

Object’s life cycle

Constructors

Let us discuss about some of the important aspects related to constructors. We all know that constructor initializes the object’s data members during object creation. Constructor is invoked during object creation and this is guaranteed by C++.

Default No-Argument Constructor

Even if your class doesn’t have an explicit constructor of its own, compiler will write a default no argument constructor for your class and invokes this upon object creation. This automatic insertion of constructor by the compiler will ONLY happen if you don’t have your own explicit constructors for your class.  If you want C++ compiler to insert default no argument constructor, then use you can ask the compiler to make one for you using ‘=default’. See the code snippet below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class A
{
private:
    int a;
public:
    A() = default;
    A(const int val): a{val}
    {
        cout << "A created: " << val << endl;
    }
};

One Argument Constructor and its Implicit Behavior

The constructor A(const int val)  is a little tricky as it has got an implicit behavior. This constructor will get invoked automatically if you try to assign a value of the type of the argument in that one argument constructor.

Let’s see how this will work for the above class A:

A a = 10;

Result:

A created : 10 

Result implies A(int) is getting called when you try to assign a value of type ‘int’. This will create an object ‘a’ of class ‘A’ from an int 10. Code like this is difficult to read, maintain and sometimes can lead to bugs. Also, why to do an implicit conversion from integer to an object of type ‘A’. To stop this implicit behavior, make use of ‘explicit’ keyword in your one argument constructor, as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class A
{
private:
	int a;
public:
	A() = default;
	explicit A(const int val): a{val}
	{
		cout << "A created : " << val << endl;
	}
};

The code “A a = 10” will not even compile now. The following error is thrown by g++, when you try it:

error: conversion from 'int' to non-scalar type 'A' requested
  A a = 10;

Constructor Delegation

What if, your class has many variants of constructors? For example, see the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A
{
private:
    int a;
    int b;
    int c;
public:    
    A(): a{0}, b{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1)
    {
        a = 0; // this code is redundant as A() does this
        b = 0; // this code is redundant as A() does this
        c = val1;
        cout << "A created : " << a << ", " << b << ", " << c << endl;
    }
};

In the above code setting ‘a’ and ‘b’ to zero is done by A(). So, similar code in A(int) is redundant. We can use C++ constructor delegation to avoid code duplication in A(int).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class A
{
private:
    int a;
    int b;
    int c;
public:    
    A(): a{0}, b{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1) : A() //, c{val1} 'c' can be initialized like this also
    {
        c = val1;
        cout << "A created : " << a << ", " << b << ", " << c << endl;
    }
};

Now lets see how it works when you run the following code :

A a(20);

Result:

A created : 0, 0, 20

Derived Classes Order of construction:

In case of an object of a derived class, the order in which constructors are called will be always starting from base class constructor followed by derived class constructor. Makes complete sense, right? Base class is also called the parent class, and derived class is also known as child class. In that case, without parent object creation, how the child object could be created, as derived class object is partly base class object as well. It is programmer’s responsibility to invoke the correct variant of base class constructor, providing the correct initial values. C++ compiler can guarantee to invoke only the default no argument constructor of base class. If your class doesn’t have a no argument constructor, then its programmer’s responsibility to invoke the correct constructor explicitly through constructor delegation. i.e. in a derived class, base class constructor can ONLY be invoked in derived class’s constructor’s member initializer list as shown below:

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
29
30
31
32
33
34
35
36
37
38
#include <iostream>

using namespace std;

class A
{
private:
    int a;
public:    
    A(): a{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1): a{val1}
    {
        cout << "A created : " << a << endl;
    }
};

class B : public A
{
private:
    int b;
public:
    B(int val1, int val2) : A(val1), b{val2}
    {
        cout << "B created : " << b << endl;
    }
};

int main()
{
    B b(10, 20);
    cout << "End of main" << endl;
    
    return 0;
}

Next, I will move on to destructors. Some of you might wonder why there is no mention about copy constructors and move constructors here? I will explain this topic later in this chapter.

Destructors in C++

I think in C++, destructor is one of the overlooked topic. Most of the beginners think destructors are there just for cleaning up the object, and everything will be taken care of automatically.  There is no need to worry about them, as the program is robust enough to handle things when objects live. When they die who cares. This is the general attitude towards destructors. But unlike other programming languages where the memory (heap) is managed by some dedicated background tasks as garbage collector in Java, in C++, it is programmer’s responsibility to manage heap. Mange heap in short, covers the course of actions you have to perform in getting memory from heap, using them in your program, and finally returning them back to heap. In C++, heap management is abstracted by an entity called the free store and primarily through free store’s new’ and ‘delete‘ operators. So, you may ask, why in C++ these things are programmer’s headache. C++ abstractions let you work right on top of the hardware without much software layering, unlike other programming languages like Java, C# and other sophisticated frameworks. You can use those languages/frameworks for certain kind of applications, but when it comes to performance critical, real time application development C and C++ are the obvious choice. Programming real time systems, OS core libraries, game engines, safety critical software that goes into automobiles, avionics systems, embedded systems such as IOT enabled devices, mission critical software used in rovers, medical appliances, life supporting devices etc are just a few that uses C and/or C++. Also, garbage collectors only guarantees to free the memory back to heap and they don’t really care about other type of resources that you have used in your object’s life-cycle, such as threads, open files, open sockets and the list goes on. i.e. Memory is not the only resource that a program manages. Such resources are still programmer’s responsibility to take care.

Now, let us see what’s so special about destructors. As you know destructors do the essential cleanup at the end of an object’s life-cycle, while constructors do the necessary initialization when the objects come alive.  But one thing you clearly need to understand is that, C++ only guarantees to invoke constructors and destructors when objects come alive and when objects life cycle come to an end, respectively. What should happen inside constructors and destructors is all up to the programmer to decide. Most of the programmers are keen in setting up the initialization values in constructors, getting resources such as threads, files, sockets etc., acquiring their handles, getting memory from heap etc. But often forget to clean them up – close open handles, return from (join) a thread etc. Some programmers do take care of such things but not in the destructors. They do these inside some other member functions and IF everything goes as planned, then their custom clean-up routine will take care of releasing the resources. What if, there is an unexpected condition, and an exception is thrown in course of execution of your program? Then there is no guarantee that your routine will ever be executed at all, and in that who will take care of those resources?

I will demonstrate the issue with the help of a small 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
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;        
    }
    
    void do_something()
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
    
    void cleanup()
    {
        if(memory != nullptr)
        {
            delete []memory;
        }
    }
};

void function_1(void)
{
    A a(10);
    
    a.do_something();
    
    a.cleanup();
    
    cout << "function_1 ends here" << endl;
    
    return;
}

Let us see what will happen?

A is created!
Operation not supported or something went wrong!
terminate called after throwing an instance of 'int'
      1 [main] destrcutors_example 12036 cygwin_exception::open_stackdumpfile: Dumping stack trace to destrcutors_example.exe.stackdump

Let us analyse what went wrong here:

  1. Executing do_something() resulted in throwing an exception
  2. C++ performed the usual stack unwinding, and found there is no code written by the programmer to handle this exception. C++ decide to call the terminate program and dumped the stack.

So, here the real unseen problem is, upon creation, object a, has acquired memory from heap. Just look at A’s constructor. But programmer has taken care of cleaning the memory in an explicit routine called cleanup(). But the problem is cleanup is never called because of an exception, and the memory is never released back to heap. Is that a leak ? Yes.

Let us use the elegant destructor now cleaning this mess up. Shall we?  

 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
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something()
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
};

void function_2()
{
    A a(50);
    
    a.do_something()
    
    cout << "function_2 ends here" << endl;
    // let us hope C++ will call the destrcutor at the end of function_2
    
    return;
}

Let’s see what will be result of executing function_2()?

A is created!
Operation not supported or something went wrong!
terminate called after throwing an instance of 'int'
      2 [main] destrcutors_example 10916 cygwin_exception::open_stackdumpfile: Dumping stack trace to destrcutors_example.exe.stackdump

Again the same thing happened. No proper cleanup. Here, what went wrong? C++ guarantees to call destructor whenever the object goes out of scope. This special code is inserted at the end of function_2(). But it never reached this point and before reaching there, it was terminated.

Let us see now how to handle this situation with a neat trick.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void function_3(void)
{
    A a(50);
    
    try
    {
        a.do_something();
    }
    catch(...)// catch all exceptions 
    {
        cout << "some exception caught" << endl;
    }
    
    cout << "function_2 ends here" << endl;
    
    return;
}

So, now what happened?

A is created!
Operation not supported or something went wrong!
some exception caught
function_2 ends here
A is destroyed

That’s it! There is a proper cleanup, destructor is called and the memory is returned back to heap. What’s the difference here?

  1. The main difference between function_2 and function_3 is in the way both dealt with exception.
  2. In ‘function_3’ an exception handler is installed using try-catch block.
  3. That caught the exception in function_3’s scope itself and resulted in full execution and return from function_3.
  4. At the end of function_3 C++’s special code to clean-up the object ‘a’, through its destructor executed and resulted in returning the memory back to heap.

noexcept keyword in C++

If you are designing a class, make sure member functions performing complex tasks involving pointer/resource manipulations are tested thoroughly. If your function doesn’t throw any exceptions, then declare it ‘noexcept’. But the problem in ‘noexcept’ is that even in case your member function results in an actual exception, no exception handlers installed in the code can ever catch that exception as the function is declared ‘noexcept’.

In the below code, class A is updated to add do_something_else() which will not throw any exceptions (well, ideally).

 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
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() 
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
    
    void do_something_else() noexcept
    {
        // do something else 
        return;
    }
};

Virtual destructors in C++

We all know when a destructor is declared as virtual it will become virtual destructor. So, what is it and why we need one. You remember the run time polymorphism I have detailed in previous chapter? Same things are applicable here. Let there be a class hierarchy with base and derived classes, and suppose you have created an instance of derived class, and your client code adheres to code to base class principle. Then, without virtual base class destructor, there is no way C++, is going to call the correct destructor and here, in this case, the correct destructor is the one defined in derived class as the object is a derived class instance.I will show this with a small example.

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

using namespace std;

class A
{
private:
    int *memory;
public:
    explicit A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    virtual void do_something() 
    {
        // do something 
    }
    
};

class B: public A
{
private:
    int *memory;
public:
    B(int size): A(size), memory{new int[size]}
    {
        cout << "B is created" << endl;
    }
    
    ~B()
    {
        cout << "B is destroyed" << endl;
        if(memory!= nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() override
    {
        return;
    }
    
};

// client code takes the ownership of 'a'
// conside main is just creating the object and giving
// complete control of the object's lifetime to client_function
void client_function(A* a)
{
    a->do_something();
    //'a' is to be deleted 
    delete a;
    
    cout << "\nclient code execution done" << endl;
    return;
}

int main()
{
    B *b = new B(10);
    client_function(b);
    
    cout << "End of main" << endl;
    
    return 0;
}

Result:

A is created!
B is created
A is destroyed
client code execution done
End of main

So, who is going to destroy B, and release back B’s memory ? This is because A’s destructor ‘~A()’ was never declared virtual, so it does not have polymorphic behavior and it’s not in v_table. So C++ when it see’s delete operation on a pointer of type A will only call A’s destructor and will not call B’s destructor. If the type itself was B, then B’s destructor will be called first and then A’s destructor in the order. But that is not the case here as client code program assumes the type of the object that it deals with is an A’s instance. And client has complete ownership of object. To avoid such a scenario, we always have to define virtual destructors in base class.

See virtual destructor in action 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
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
#include <iostream>

using namespace std;

class A
{
private:
    int *memory;
public:
    explicit A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    virtual ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    virtual void do_something() 
    {
        // do something 
    }
    
};

class B: public A
{
private:
    int *memory;
public:
    B(int size): A(size), memory{new int[size]}
    {
        cout << "B is created" << endl;
    }
    
    ~B()
    {
        cout << "B is destroyed" << endl;
        if(memory!= nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() override
    {
        return;
    }
    
};

// client code takes the ownership of 'a'
// conside main is just creating the object and giving
// complete control of the object's lifetime to client_function
void client_function(A* a)
{
    a->do_something();
    //'a' is to be deleted 
    delete a;
    
    cout << "\nclient code execution done" << endl;
    return;
}

int main()
{
    B *b = new B(10);
    client_function(b);
    
    cout << "End of main" << endl;
    
    return 0;
}

Result:

A is created!
B is created
B is destroyed
A is destroyed

Resource Acquisition Is Initialization (RAII)

In the above code, did you notice that in class A’s constructor A(int size), memory is acquired from heap? Also, in A’s destructor the acquired memory is returned back to heap. This is RAII all about. That is to acquire the resource in constructor and to release it, cleanup in destructor, thus avoiding ‘naked new‘ and ‘naked delete‘ .i.e. RAII is making use of two things that C++ guarantees.

  1. C++ guarantees to invoke the constructor at object’s creation
  2. C++ guarantees to invoke the destructor when object’s lifecycle ends

The above two things are mechanisms of C++. RAII is a policy which puts to use these mechanisms in a very elegant and smart way. It is RAII and not MAII. That means, its applicable for any resource, and is not just limited to memory, but extends it to other resources such as threads, files, sockets etc. Bjarne Stroustrup has discussed about this topic nicely in his books – “A Tour of C++” and “The C++ Programming Language”.

Here are the important aspects of RAII

  1. Resources should be acquired as part of constructing the object itself. i.e in constructors.
  2. Constructors should implement class invariants or some condition checks that will prevent any invalid resource acquisition. For instance, in case of memory slots, the invariant can be whether the size parameter is not negative.
  3. In case the check fails, constructors should throw exception, thus indicating object creation has failed.
  4. And lastly, any acquired resource must be cleaned up, released in the destructors, in the reverse order that they were acquired in constructors. Last acquired should be released first.

Thus, RAII is all about managing resources as part of object’s life-cycle itself.

The fundamentals of copy and move semantics in modern C++

Copy and Move Semantics in modern C++

What does it mean to copy and move objects? In the literal sense, copy mean to clone an object to a new object, and make an exact replica of the original object. If object ‘a’ of class ‘A’ is copied to object ‘b’, then object ‘b’ can be treated as an exact clone/replica of object ‘a’. For copying objects, there are two ways in C++.

  1. To create a new object as a clone of an existing object
  2. To copy the contents of an existing object to another existing object, thus making a clone.

Copy constructor:

In the first approach, since a new object has to be created, we will go with constructors as it is the only way to create an object. But here, we are creating a clone or copy of another existing object. So, this special constructor should take this existing object as its parameter and read the contents of that object to this newly created object byte by byte. I repeat byte by byte, as we don’t want a shallow copy but a deep copy. This special constructor is known as copy constructor in C++. I’ll demonstrate this with an example. I will walk you through a small Array class to demonstrate copy constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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];
	}
}

Did you notice a special constructor in the code above?

1
Array(const Array& other_object);

The constructor takes exactly one argument, a constant reference to another object of the same class. Let’s examine what all things it is doing inside:

  1. Copies the size_ data from the other object
  2. Acquires a new memory region from free store using new operator to store size_ number of integers.
  3. Copies the data stored in other object’s memory one by one.

Steps 2 and 3 are more important here, as it is just not a shallow copy where the other object’s memory address is alone copied. In deep copy, we allocated a new memory and then copied the members from other objects memory one by one. This is important to note here. i.e the difference between shallow copy and deep copy. Now, let us see the result of the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Array a(10);

for(int index = 0; index < a.size(); ++index)
{
	a[index] = index;
}

// lets copy construct object 'b' from 'a'
Array b(a);

for(int index = 0; index < b.size(); ++index)
{
	cout << "b[" << index << "] : " << b[index] << endl;
}
Array is created with size 10
Array object's clone is created
b[0] : 0
b[1] : 1
b[2] : 2
b[3] : 3
b[4] : 4
b[5] : 5
b[6] : 6
b[7] : 7
b[8] : 8
b[9] : 9
Array is destroyed
Array is destroyed

The default copy constructor

If you don’t write a copy constructor for your class, C++ compiler will always insert one default copy constructor in your class that will look exactly same as the one you have explicitly written above, with ONLY ONE difference. i.e. The default copy constructor can copy only the objects data members. In this case Array class has only two data members declared; one is a ‘size_’ data which is just 4 bytes-integer, and other one is a pointer data ‘memory’, which is another 4 bytes. So, default copy constructor will just copy these 8-bytes from other object to the newly created object. In effect, both the objects will point to the exact same memory region in heap. Thus, the new object is not a clone of the other object or we can say that copy did not work. This is a typical example of shallow copy.

Shallow copy vs deep copy

I will show you the difference between shallow copy and deep copy with the help of diagrams.

The image above is how shallow copy works. C++ default copy constructor does this.

The above diagram shows how a deep copy construction is performed.

Copy assignment operator’=’

This is the second approach I have mentioned above, where there is no new object created. What happens is, an existing object will get copied to another existing object. So, if there is some data already present in the copied to object it will be all lost and will get updated with data inside copied from object. Make sure in the copied to object there is no data present or if there is some data then it should be taken care before copy.

I will demonstrate how copy assignment operator is done in C++.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// copy assignment operator 
Array& operator=(const Array& other_object)
{
    if(this == &other_object)
    {
        return *this;
    }
    
    cout << "Array object is being cloned" << endl;
    
    // dont allocate a new memory, 
    // use the heap memory allocated to existing object
    
    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;
}

Now what will happen if you do the following?

1
2
3
4
5
6
7
8
Array c(10);
// 'c' is already created here
c = a;

for(int index = 0; index < c.size(); ++index)
{
	cout << "c[" << index << "] : " << c[index] << endl;
}

Want to see the result? Before that did you notice something – during copy assignment, I did not allocate a new memory for the existing object as memory is already allocated, to the object. Now, let’s see the result of the above code:

Array is created with size 10
Array object is being cloned
c[0] : 0
c[1] : 1
c[2] : 2
c[3] : 3
c[4] : 4
c[5] : 5
c[6] : 6
c[7] : 7
c[8] : 8
c[9] : 9

You can see that object ‘c’ has become a clone of object ‘a’ as a result of copy assignment operation. But here, there is an issue. What if the size of the copied from is greater than that of the existing object’s ? Say, I have an object of 20 integers and tried to copy to another object created for storing just 10 integers. We can have a modified copy assignment operator.

Let us see a modified version of copy assignment operator for our class Array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Array& operator=(const Array& other_object)
{
    if(this == &other_object)
    {
        *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;
}

Now, what happens if we execute the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Array a(20);

for(int index = 0; index < a.size(); ++index)
{
	a[index] = index;
}

Array c(10);

// 'c' is already created here
c = a;
for(int index = 0; index < c.size(); ++index)
{
	cout << "c[" << index << "] : " << c[index] << endl;
}

Let us see the result:

Array is created with size 20
Array is created with size 10
Array object is being cloned
c[0] : 0
c[1] : 1
c[2] : 2
c[3] : 3
c[4] : 4
c[5] : 5
c[6] : 6
c[7] : 7
c[8] : 8
c[9] : 9
c[10] : 10
c[11] : 11
c[12] : 12
c[13] : 13
c[14] : 14
c[15] : 15
c[16] : 16
c[17] : 17
c[18] : 18
c[19] : 19

See that object ‘c’ is first created just for 10 integers. Copy assignment operator has now reallocated a new memory for storing 20 integers in ‘c’ and then copies all 20 members from other_object, which is here object ‘a’.

One important thing is there to note. In copy assignment operation do not try to copy the same object to itself. We can introduce a self-copy check guard. Use the following check before starting any copy operation, in copy assignment:

‘if(this == &other_object) {}’ , this block will get executed if both objects are same. In this case simply return the same object without any change.

Move semantics in C++ 11: r-value and r-value reference in modern C++

So, what is r-value reference? It is just like any other reference, but here the referred object/data is a temporary value. The best candidates for temporary values are function return values, right hand side of some complex expression which may result in generating temporary objects/data etc. The opposite of r-values are l-values. So from the naming convention they have used here, we can surmise that l-values are those which are present on the left hand side of an expression, and r-values are those present on the right hand side of an expression. Till C++11 there was no support to get r-value references; References could only be made to proper variables/objects/data stored in addressable memory. Actual r-values cannot be referred as their addresses cannot be obtained.

Say,

1
2
int y = 5;
int x = function(y);

Here, it is not possible to get the address of the returned value from function(y) in the expression ‘int x = function(y);’ So this is an example for r-value.

Now, in C++ 11, they have introduced r-value references. For telling the compiler that this data type is an r-value reference just use the syntax ‘&&’. We use a single ‘&’ for declaring l-value references in C++. So for r-value use two &’s i.e. ‘&&’ . Don’t mistake ‘&&’ as reference to reference. There is no such thing in C++. Some of you may think that since there are pointers to pointers in C++, why can’t the same be applied for references. Just treat ‘&&’ as r-value reference and NOT as reference to reference.

Move semantics in modern C++, works only with r-value references. Move means to move from one place to another. Here, it means to move something present inside one object to another object. In operating systems, you must have come across ‘cut’ and ‘paste’ which is analogous to move operation in C++. Yes, so copy means ‘copy’ and ‘paste’, of course. But the point here is, after a ‘cut’ and ‘paste’ operation the original entity (file/text/image) will not be present in its old place and will be moved /shifted to its new location. In C++ move operation, we have to explicitly destroy the content of the original object, but indeed NOT the object itself.

Let us see what happens during move in C++:

See how objects look like before and after move operation. Here a temporary object ‘a’ has been moved to ‘b’, and post move object ‘a”s contents is cleaned to a valid default state.

Some points to note about move semantics:

  1. You cannot move an object with a constant data member. Because you cannot cleanup the constant value post move
  2. You cannot move-assign any objects with references, as references once initialized during object construction, cannot be re-assigned with any values.

Move constructor

Now that, you have understood what move basically means, let us go through the code for implementing move constructor. So here, something is moved from an existing object to a new object just being created. That’s why its move constructor, just like copy constructor.

Let’s see move constructor in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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;
}

The interesting syntax of move constructor:

  1. The object is passed as an r-value reference. i.e. ‘Array && other_object’
  2. Second most important thing here is, there is no ‘const’ in the parameter. Of course, the move construction will eventually destroy the contents of other_object then how to declare it as constant.

Let us see how the below code will execute:

1
2
3
4
5
6
Array d(std::move(a)); // std::move converts l-value to r-value.

for(int index = 0; index < d.size(); ++index)
{
	cout << "d[" << index << "] : " << d[index] << endl;
}

Move constructor: Array has been moved!
d[0] : 0
d[1] : 1
d[2] : 2
d[3] : 3
d[4] : 4
d[5] : 5
d[6] : 6
d[7] : 7
d[8] : 8
d[9] : 9
d[10] : 10
d[11] : 11
d[12] : 12
d[13] : 13
d[14] : 14
d[15] : 15
d[16] : 16
d[17] : 17
d[18] : 18
d[19] : 19

Here, object ‘d’ is move-constructed from object ‘a’. Object ‘a’ is not a r-value, but std::move() will convert l-value to r-values. That’s all!

NOTE: std::move() will not do the move operation itself. It will just convert l-values to r-values.

Move assignment operator

Just like ‘copy assignment’ operator, there is its ‘move’ counterpart. Let us see this in code, and you will understand without further explanation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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;
}

Make sure to release back the old memory acquired in the moved to object back to heap. See line number 11 in the above code.

Let us see the result of code shown below:

1
2
3
4
5
6
7
Array e(10);
e = std::move(d);

for(int index = 0; index < e.size(); ++index)
{
	cout << "e[" << index << "] : " << e[index] << endl;
}

In the above code, an existing object ‘d’ has been moved to ‘e’. Note that we have used std::move() on ‘d’ to make it an l-value.

Array is created with size 10
An Array object is moved to this Array object
e[0] : 0
e[1] : 1
e[2] : 2
e[3] : 3
e[4] : 4
e[5] : 5
e[6] : 6
e[7] : 7
e[8] : 8
e[9] : 9
e[10] : 10
e[11] : 11
e[12] : 12
e[13] : 13
e[14] : 14
e[15] : 15
e[16] : 16
e[17] : 17
e[18] : 18
e[19] : 19

So that’s it folks, we have covered copy and move semantics in modern C++.

If you want to see the Array class implementation in its full glory and the complete code for testing the copy/move operations, see 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
 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
#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_;
    }
    
};

int main()
{
    Array a(20);
    
    for(int index = 0; index < a.size(); ++index)
    {
        a[index] = index;
    }
    
    // lets copy construct object 'b' from 'a'
    Array b(a);
    
    for(int index = 0; index < b.size(); ++index)
    {
        cout << "b[" << index << "] : " << b[index] << endl;
    }
    
    Array c(10);
    // 'c' is already created here
    c = a;
    
    for(int index = 0; index < c.size(); ++index)
    {
        cout << "c[" << index << "] : " << c[index] << endl;
    }
    
    Array d(std::move(a)); // std::move converts l-value to r-value.
    
    for(int index = 0; index < d.size(); ++index)
    {
        cout << "d[" << index << "] : " << d[index] << endl;
    }
    
    Array e(10);
    e = std::move(d);
    
    for(int index = 0; index < e.size(); ++index)
    {
        cout << "e[" << index << "] : " << e[index] << endl;
    }
    
    
    return 0;
}

Before concluding this session on copy and move, in C++ there are default copy, move constructors and assignment operators. I have already explained the issue of shallow copy with the default copy constructor in this chapter. It is strongly suggested to write your own explicit copy/move constructors and assignment operators for your class if it has some resource management related things as seen in the ‘Array’ class presented in this chapter. Also, one more thing. If you don’t want your class to support copy/move operations, ask the compiler to delete these operations from your class by using ‘=delete’. For example, suppose you have a class My_Class, for which you don’t want to have any copy and move operations. Do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class My_Class
{
    My_Class()
    {
        
    }
    ~My_Class()
    {
        
    }
    
    My_Class(const My_Class& other_object) = delete;
    My_Class& operator=(const My_Class& other_object) = delete;
    My_Class(My_Class&& other_object) = delete;
    My_Class& operator=(My_Class&& other_object) = delete;
    
};

On the contrary, if you want the compiler to generate these for you then use ‘=default’ for the required operation. The thing to note here is only the operation you have selected will get generated by the compiler, while others will not be generated at all. i.e if you default generate copy constructor, then copy assignment, move constructor and assignment operations will not be default generated by the compiler.

I have not explained how copy/move semantics should be applied for class hierarchies so far in this chapter. This topic will be discussed in next chapter. For now, let’s concentrate on copy/move for normal classes without any derived class hierarchies.

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

0

Chapter 5 : The Philosophy Of C++, Part A

Key words: The Philosophy of C, The Philosophy of C++, Foundations of C++, object orientation in C++, class in C++, class vs object in C++, size of an object, inheritance and class hierarchies, compile time polymorphism, run time polymorphism, Early binding and late binding in C++, Virtual table and the virtual pointer in C++, Name mangling in C++, static member functions, static data members in C++, static data declaration vs initialization, The enum class of C++, operator overloading and friend functions

Topics at a glance:

  • Introduction : The foundations of classical C++
  • Classes – defining your own types and establishing behaviors
  • Objects – manifestation of behaviors, achieving uniqueness
  • How big an object really is ?
  • Code Reuse – Inheritance, late binding and achieving polymorphism
  • Name mangling and how to decrypt C++’s generated mangled naming convention
  • Code to base class idiom
  • Static classes, Static member functions, initializing, working and their intricacies
  • Making friends, putting enums to use, introduction to operator overloading

C++ is a complex and a powerful programming language. Yes, it is complex, but once you start to understand the philosophy behind the features it provides, the ways it follows to achieve things, then you will begin to understand that C++ is much simple than you initially thought.

Let us understand the foundation on which C++ is built on., and it’s philosophy. We all know that C++ originated as a derivative of ‘C’ language. So to understand C++’s philosophy one must understand C’s philosophy first.

The Philosophy of C

The philosophy of ‘C’ programming language lies on how it abstracts various data types, and how it’s execution model works. For this, you need to understand how various data types such as int, char, float, user defined data types such as arrays, structs, unions are stored and manipulated/used in ‘C’. How the byte ordering such as little and big endian systems differ and how C approaches both these systems, (Chapters 1, 2, 3), how the stack is used in ‘C’ and how it is used in its execution model (i.e. stacks and functions, as I have explained in detail in Chapter 4), and should have an understanding of different sections of a C program such as data segment, text segment, and heap in general. Once you understand these then you will get a grasp of the Philosophy of ‘C’, the ideology its built on.

The Philosophy of C++

Philosophy of classical C++ is realized in three aspects.

Foundations of classical C++

1: It is essential that you understand C’s philosophy first, before venturing into C++. This is the first philosophical aspect of C++ – i.e. The philosophy of ‘C’.

2: C++ is not just ‘C’. C++ gives you more sophisticated abstraction mechanisms that helps a programmer to define his own data types, to establish their behavioral attributes; This is the foundation on which C++’s support for Object Oriented Programming is based on. OOP is the second philosophical aspect of C++.

3: The third philosophical aspect of C++ is its immense capability for supporting generic programming via templates.

In this chapter we will focus on the C++’s OOP aspect realized through the abstraction called class and it’s attributes.

Note: For explaining C++ concepts, semantics and idioms I have used C++11 or C++14 throughout my blog. I strongly recommend everyone who really want to use C++ to it’s fullest potential to adopt modern C++ i.e C++11, 14, 17, or 20.

Object orientation in C++

Classes and Objects in C++

C++ realizes OOP and its concepts such as encapsulation, inheritance, and various manifestations of polymorphisms through a different category of abstraction known as class. Without class there is no OOP in C++. Class helps one to define his own data type, and define a behavior to that data type. Instances of a class are known as objects. Object is a realization of a class with an attribute, that is, its uniqueness. Object instances are distinct from one another even though they belong to the same class (type). Class definition defines common behavior for all of its object instances. i.e. the methods/member functions defined in a class helps in implementing the behavior and object instances personify its class and exhibits uniqueness through the data it encapsulates. To explain how a class’s behavior is different from object’s uniqueness I will use the following example.

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

using namespace std;

class Integer_Pair
{
protected:
    int i_;
    int j_;
public:
    // constructs an instance of Integer_Pair with provided values 
    Integer_Pair(int i = 0, int j = 0): i_{i}, j_{j}
    {
        cout << "An Object of Integer_Pair is instantiated with pair(" 
             << dec << i_ << ", " << j_ << ") " << endl;
    }
    
    int add() const
    {
        return i_ + j_;
    }
    
    int subtract() const 
    {
        return i_ - j_;
    }
    
    int multiply() const 
    {
        return i_ * j_;
    }
    
    double divide() const  
    {
        return ( static_cast<double>(i_)/static_cast<double>(j_) );
    }
    
    ~Integer_Pair()
    {
        cout << "Object - (" 
             << dec << i_ << ", " << j_ << ") " 
             << "has been destroyed!" << endl;
    }
};

class Integer_Pair_2 : public Integer_Pair
{
    // inheriting Base class constructor. 
    // No need of explicit constructor for derived class Integer_Pair_2
public:
    using Integer_Pair::Integer_Pair;
    void print()
    {
        cout << "(" << dec << Integer_Pair::i_ << ", " << Integer_Pair::j_ << ")" << endl;
    }
};

int main()
{
    Integer_Pair ip_1(1, 2);
    Integer_Pair ip_2(5, 5);
    
    // Now to show you how unique ip_1 and ip_2 are 
    cout << "ip_1's behavior : " << endl;
    cout << "ip1.add() gives " <<  ip_1.add() << endl;
    cout << "ip1.subtract() gives " <<  ip_1.subtract() << endl;
    cout << "ip1.multiply() gives " << ip_1.multiply() << endl;
    cout << "ip1.divide() gives "  << ip_1.divide() << endl;
    
    cout << "\nip_2's behavior : " << endl;
    cout << "ip2.add() gives " << ip_2.add() << endl;
    cout << "ip2.subtract() gives " << ip_2.subtract() << endl;
    cout << "ip2.multiply() gives " <<  ip_2.multiply() << endl;
    cout << "ip2.divide() gives "  << ip_2.divide() << endl;
    
    cout << "\nip_1 is stored @ " << hex << &ip_1 << endl;
    cout << "ip_2 is stored @ " << hex << &ip_2 << endl;
    
    // observe the trick to get the address of a class member function in C++ 
    int (Integer_Pair::*ptr_to_add)() const = &Integer_Pair::add;
    int (Integer_Pair::*ptr_to_multiply)() const = &Integer_Pair::multiply;
    
    cout << "address of add and multiply are as : "
         << hex  << (void*&)ptr_to_add 
         << ", " << (void*&)ptr_to_multiply
         << endl;
        
    cout << "\nsizeof(ip_1): " << dec << sizeof(ip_1) << endl;
    cout << "sizeof(ip_2): " << dec << sizeof(ip_2) << endl;
    
    cout << endl;
    
    Integer_Pair_2 ip_a_1(4,3);
    ip_a_1.print();
    
    cout << "sizeof(ip_a_1) : " << dec << sizeof(ip_a_1) << endl; 
    
    int (Integer_Pair::*ptr_to_add_parent)() const = &Integer_Pair::add;
    int (Integer_Pair::*ptr_to_add_derived)() const = &Integer_Pair_2::add;
    
    
    cout << "\naddress of add in parent class Integer_Pair: "
         << hex  << (void*&)ptr_to_add_parent 
         << "\naddress of add in derived class Integer_Pair_2: "
         << hex  << (void*&)ptr_to_add_derived << "\n"
         << endl;
    
    return 0;
    
}

A general advice: please restrain from using ‘endl’ whenever you want a new line in a text. Use ‘\n’ if possible, as ‘endl’ also flushes out the output buffer of the ostream. Use ‘endl’ at end of lines, of course.

Instance of class Integer_Pair can store two integers thus the name Integer_Pair. Behaviors for adding, subtracting, multiplying and dividing is defined by the class. That means any object instance of the class exhibits these behaviors.

Result:

An Object of Integer_Pair is instantiated with pair(1, 2)
An Object of Integer_Pair is instantiated with pair(5, 5)
ip_1's behavior :
ip1.add() gives 3
ip1.subtract() gives -1
ip1.multiply() gives 2
ip1.divide() gives 0.5
ip_2's behavior :
ip2.add() gives 10
ip2.subtract() gives 0
ip2.multiply() gives 25
ip2.divide() gives 1
ip_1 is stored @ 0x461cc48
ip_2 is stored @ 0x461cc40
address of add and multiply are as : 0x4020e0, 0x402118
sizeof(ip_1): 8
sizeof(ip_2): 8
An Object of Integer_Pair is instantiated with pair(4, 3)
(4, 3)
sizeof(ip_a_1) : 8
address of add in parent class Integer_Pair: 0x4020e0
address of add in derived class Integer_Pair_2: 0x4020e0
Object - (4, 3) has been destroyed!
Object - (5, 5) has been destroyed!
Object - (1, 2) has been destroyed!

Now, coming to an object’s uniqueness. We can see that result of the above program execution, shows that even though objects’s behavior is same, the results from object 1 (i.e. ip_1) is distinct from that of object 2 (i.e. ip_2).

Behavior simply defines what the class is going to execute, when you invoke the associated member function. Whereas, the exact manifestation of the behavior depends on the state of the object on which the member function is invoked.  ‘ip_1.add()’ resulted in 3, which is the result of 1 + 2. That means object ip_1’s state is just defined by the values it stores, i_ and j_. Now you know why ip_2.add() resulted in 10, as state of ip_2 is defined by i_ at 5 and j_ at 5. When you invoke add() on ip_1 or ip_2 it executed addition of the pair of numbers that the two objects store. i.e. How the behavior manifests depend upon the objects state defined by the data that the objects store in memory.

Two things are here to note; a class defining a set of common behavior, and there are objects with unique states. So, how this is possible in C++ through class abstraction? Simple. Instances of objects are allocated in distinct memory locations. Common behavior which is achieved via member functions of a class are not replicated for each object but only one instance exists for all the objects of a class, and will be allocated to, what is called as the, text segment.

To illustrate this, the main() also prints the storage locations of ip_1, ip_2 and the member functions as well. We see that both ip_1 and ip_2 object sizes are same and is 8 bytes. These are used for storing i_ and j_. So, where are the member functions stored? They are stored in the text segment somewhere else. The point here to note is wherever they are stored, the member functions are not stored where objects are stored. This is the difference you need to understand.

Don’t divert your focus into the complex syntax I have used to get the address of member function. Just see that addresses of add and subtract are obtained without even using the objects. In C++, you cannot get member function address through an object instance. Because, object doesn’t store member function address in it’s memory. Remember object size in above code is just 8 bytes used for storing it’s data i_ and j_? For getting member function address, use the class name, in conjunction with C++ scope resolution operator :: along with some other tricks which I have depicted in the code above.

Inheritance and class hierarchies

Code reuse via inheritance:

We use inheritance mainly to extend or improvise or for adding extra features for an existing class. I will show you the exact meaning of code reuse in C++. In fact, C++ has taken the literal meaning of code reuse.

I have derived a class Integer_Pair_2 from Integer_Pair. Integer_Pair_2 introduce an extra member function print. Since it doesn’t have any data members of its own, Integer_Pair_2 doesn’t need an explicit constructor. So I am inheriting the constructor from base/parent class with the help of C++ 11 feature – ‘using’. NOTE: I have made access specifier for Interger_Pair parent class’s i_ and j_ to protected, so that derived class can access these members.

Please focus on the following lines of the above result,

An Object of Integer_Pair is inistantiated with pair(4, 3)
(4, 3)
sizeof(ip_a_1) : 8
address of add in parent class Integer_Pair: 0x4020e0
address of add in derived class Integer_Pair_2: 0x4020e0

Both classes use the exact same function implementation of add() stored at 0x4020E0.

That means, derived class doesn’t even create an unnecessary copy of member functions that it inherits from parent class. Also, look at the object size of ip_a_1. its still 8 bytes as Integer_Par_2 doesn’t add its own copies of data members in the hierarchical class system, object instances still use 8 bytes for storing i_ and j_.

Size of an object

Size of object is the size of data members declared in the class of that object. If the object is an instance of a hierarchical class system ( i.e. inherited class system), then, the size of object is  equal to the combined size of all data members of all the classes it is derived from. Also, if the class is polymorphic, then you will have to consider the size of a pointer to virtual table as well. This v_table pointer, typically adds 4 more bytes in a 32-bit system in addition to the size of data members declared in the hierarchical class system.

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

using namespace std;

class Integer_Pair
{
protected:
	int i_;
	int j_;
public:
	// constructs an instance of Integer_Pair with provided values 
	Integer_Pair(int i = 0, int j = 0): i_{i}, j_{j}
	{
		cout << "An Object of Integer_Pair is inistantiated with pair(" 
		     << dec << i_ << ", " << j_ << ") " << endl;
	}
	
	int add() const
	{
		return i_ + j_;
	}
	
	int subtract() const 
	{
		return i_ - j_;
	}
	
	int multiply() const 
	{
		return i_ * j_;
	}
	
	virtual double divide() const  
	{
		return ( static_cast<double>(i_)/static_cast<double>(j_) );
	}
	
	~Integer_Pair()
	{
		cout << "Object - (" 
		     << dec << i_ << ", " << j_ << ") " 
			 << "has been destroyed!" << endl;
	}
};

class Swapped_Pair : public Integer_Pair
{
	
public:
	using Integer_Pair::Integer_Pair;
	//using Integer_Pair2::Integer_Pair;
	double divide() const override
	{
		return (  static_cast<double>(j_) \
		         /static_cast<double>(i_) );
	}
};

// ip_obejct is a reference to an object of the type Inetger_Pair 
void integer_pair_client(Integer_Pair &ip_object)
{
	// add is not a virtual function so this invokes base class add()
	cout << "\nip_object.add() : " << dec << ip_object.add() << endl;
	// divide is polymorphic, so this invokes derived calss divide 
	cout << "ip_object.divide() : " << ip_object.divide() << endl;
	
	return;
}

int main()
{
	Integer_Pair ip_1(1, 2);
	Swapped_Pair ip_b_1(1,2);
	
	cout << "\nip_b_1.divide : " << ip_b_1.divide() << endl;
	cout << "ip_1.divide : " << ip_1.divide() << endl;
	
	cout << "\nsizeof(ip_1): " << dec << sizeof(ip_1) << endl;
	cout << "sizeof(ip_b_1): " << dec << sizeof(ip_b_1) << endl;
	
	//Code to the base class principle 	
	integer_pair_client(ip_b_1);
	
	cout << endl;
	
	return 0;
	
}
An Object of Integer_Pair is inistantiated with pair(1, 2)
An Object of Integer_Pair is inistantiated with pair(1, 2)
ip_b_1.divide : 2
ip_1.divide : 0.5
sizeof(ip_1): 12
sizeof(ip_b_1): 12
ip_object.add() : 3
ip_object.divide() : 2
Object - (1, 2) has been destroyed!
Object - (1, 2) has been destroyed!

If you want the result of j_ / i_ i.e. j_ divided by i_, instead of i_ divided by j_ that the parent class Integer_Pair implements, then instantiate an object of class Swapped_Pair and see how divide works. That means divide method of parent class is now overridden by the derived class.

That’s why, when divide is invoked on ip_b_1, which is an object of class Swapped_Pair, the result is 2, i.e. (2/1). But the same on ip_1 results in 0.5 i.e. 1/2.

The interesting thing to notice here is why the size of objects, both ip_1 and ip_b_1 is now 12 bytes and not 8 bytes. This is the result of a pointer to virtual table, a.ka. pointer to v_table or vptr that I mentioned above. But you may ask why compiler inserts one? For answering this, we need to understand, how the compiler correctly picks the right variant of function divide? The derived class variant or base class variant? Please continue the following sections to understand what’s happening under the hood.

Early binding and Late binding in C++

Have you ever wondered how a call for divide on ip_b_1 and ip_1 invokes the correct variant of divide, despite the similarity in their name, parameter types, and return type? This is achieved through a concept known as late binding in C++. In C and C++ all normal functions, when compiled generates unique symbols, eventually becoming part of symbol table. When compiler finds any reference to these functions in code, it can easily map that reference to a specific function directly identified by its unique symbol from the symbol table. This deduction is done completely by the compiler at the compilation stage. This is also known as static binding, where the compiler at compile time deduce and hence binds the actual function from symbol table to wherever a caller has made a reference to the function in the code. Once you generate the assembly code for a normal function call in ‘C’ or ‘C++’ you will understand this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

// normal function in C and C++
int increment(int arg)
{
	// do some computation on arg 
	// and return the result
	return (arg + 1);
}

int main()
{
	int number = 4;
	// invoke the increment function
	number = increment(number);
	
	return 0;
}

The portion of assembly (relevant to this topic), generated for this by gcc ‘C’ compiler and g++ ‘C++’ compiler is shown below:

‘C’ compiler generated assembly:

_main:
; do the necessary stack frame settings
call	_increment
_increment:
; do the necessary stack frame settings
; do the computation, store the result
; do the necessary stack reverse and return

‘C++’ compiler generated assembly

_main:
; do the necessary stack frame settings
call	__Z9incrementi
__Z9incrementi:
; do the necessary stack frame settings
; do the computation, store the result
; do the necessary stack reverse and return

One thing is common here. The compiler knows which function to call. Let us see how it is achieved in C++.

Compile time polymorphism

You would have noticed the symbols i.e. the function name symbols generated by C and C++ compilers for the same source code differs. For C, its ‘_increment’ while in C++, its some mysterious ‘__Z9incrementi’.

Name mangling in C++

This mysterious symbol naming generation in C++ is known as name-mangling. First I will explain how to decrypt the name __Z9incrementi.

Name part

meaning

‘__Z’

Indicates that the name is managed by C++ compiler; avoids conflicts with other user defined symbols present throughout the code

‘increment’

The name user defined in C++ source code

‘i’

First argument is an integer therefore its ‘i’; For char it will become ‘c’, for void it will become ‘v’ and so on.

‘9’

I have kept this towards the last because you can easily understand now what’s ‘9’. It’s just the string length of the user defined name ‘increment’. So that after parsing 9 characters of the symbol, the remaining characters will be treated as part of parameter identifiers, in this case 10th character is ‘i’ for integer parameter.

 

Now, you understand how to decrypt the C++ name mangled symbols. The question is why is this necessary? C++ supports something known as function overloading. That meas, defining functions with exactly same name in the same scope, but with different behavior. It is easy to explain this with the help of an example of function overloading in C++.

Function Overloading in C++
 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
#include <iostream>

using namespace std;

int increment(int number)
{
    return (number + 1);
}

int increment(int number, int count)
{
    return (number + count);
}

char increment(char c)
{
    return (c + static_cast<char>(1));
}

int main()
{
    int number = 4;
    // invoke the increment function
    number = increment(number); // does 4 + 1
    
    cout << "Number is " << dec << number << endl;
    
    number = increment(number, 5); // does 5 + 5
    
    cout << "Number is " << dec << number << endl;
    
    char c = 'A';
    c = increment(c); // does 'A' + 1
    
    cout << "Character is " << c << endl;
    
    return 0;
}

The above example, shows increment function overloaded for supporting integer and character, and one variant that takes one extra parameter to specify the amount by which the input should be incremented.

So, in the above code, how compiler will understand which function to call? By checking the number of parameters, and type of parameters in addition to the function’s user defined name itself. At least, some would have wondered why the return type is never considered in name mangling by C++? The answer is simple, return value is not necessary to be used by the caller/invoker? What if the caller simply invokes the function – increment() without taking in the returned value; also assume that all versions of overloaded functions uses the same number and type of parameters.

Let us see how name mangling can help C++ compiler to resolve the symbols in case of function overloading. These are the g++ generated mangled names for the above variants of the function increment:

Mangled name

Function variant declared in C++ source

__Z9incrementi

int increment(int);

__Z9incrementii

int increment(int, int);

__Z9incrementc

char increment(char);

 

You can see how unique symbols are generated for the three function variants. Name mangling, thus, avoids any confusion while deducing the specific variant of the function that the caller has invoked/referred. This kind of binding symbols, to the places where they are referred in code, by the compiler at compile time itself is called static binding or compile time binding or simply early binding as it happens at an early stage before loading and running the program.

Now let us focus on to the counterpart of early binding which is dynamic binding/late binding/run time binding.

For function overloading, which is a type of compile time polymorphism in C++, name mangling can help the compiler in deducing the exact variant.

Run time polymorphism

Now let us turn our attention to run time polymorphism supported by C++ using function overriding by derived classes? There, the function name, return type, parameter type and number of parameters are all exactly the same in base class and its variant re-defined in derived class. Remember the member function divide() of Integer_Pair and Integer_Pair_2?

This time, C++ cannot do early binding and it uses something known as run time/late binding. This is achieved in two phases. Phase one is done by the compiler itself, and phase 2 is manifested during run time as a result of phase 1.

Phase 1 of Late Binding (compiler stage):

Virtual table and the virtual pointer in C++

In C++ polymorphic member functions are getting called always via the v_table. Simply put, v_table is just an array of pointers. (There are other Run Time Type Identification (RTTI) information packed into v_table, but for the time being let us not deviate and stick to just the use of v_table as an array of pointers). v_tables are created per polymorphic class. During the construction of an object instance of a polymorphic class such as the one we have used here, the compiler, in addition to allocating memory for storing the object, also allocates additional memory for storing a pointer to the v_table. For objects of a base class, the addresses of base class v_table is added to this v_ptr. But in case of derived class object, this v_ptr gets filled first with the addresses of base class v_table and then it will get overwritten with the address of derived class v_table. Remember the order of constructor invocation. So, regardless of the number of polymorphic functions defined in the class, object size is incremented by an extra 4-bytes for storing this vptr.

That means v_table is not inside an object’s memory, but outside. And object just carries in its v_ptr an address to it’s class’s v_table.

Two things to note here:

  1. v_table is class specific and is always one v_table per one polymorphic class. (i.e. classes with virtual functions).
  2. v_ptr is per object and points to the actual class type of the object.

Phase 2 of Late Binding (run time stage):

Whenever the prog ram invokes any polymorphic function, a special code inserted by the compiler comes into action. This special code goes through the v_table accessed via the object’s vptr, which will in turn, point to the address of correct variant of the polymorphic function. i.e. Base class variant or derived class variant. The only hurdle here is when the polymorphic function is also an overloaded function. Therefore, at this stage, the special code also checks the correct function signature using the name mangling information and finally finds the correct function address from the v_table and invokes it through the address. 

This procedure is a kind of a callback function. In a way, it is similar to how embedded programmers program interrupt handlers. Upon any interrupt, the interrupt source is conveyed by certain CPU registers, along with some additional information, which will be used, to find the correct interrupt handler. This involves callback function along with deducing the correct function to be called, similar to phase 2 of late binding in C++.

Steps in late binding for achieving run time polymorphism.

  1. Compiler creates a v_table per class (separately for base class and derived class(es) ) which can hold the polymorphic function addresses. For base class, this v_table will have base class function addresses and for derived class it will have derived class function addresses.
  2. During object creation, one v_ptr data member per object is added in the object’s memory, which will points to the class specific v_table. As base class constructor is run first, v_ptr will be initialized to base classes’ v_table address. When derived class constructor is run, this v_ptr will get overwritten with derived v_table address. Remember the order of invoking constructors for derived class objects. Base first and then derived.
  3. Throughout the code, wherever compiler sees any reference to any function (either polymorphic or non-polymorphic) , the function signature is deduced using name mangling information.
  4. Compiler finally adds special code for accessing the v_table through object’s v_ptr.
  5. This is where the compiler stage completes. Or let’s say, phase 1 of late binding is done here.
  6. Now, during run time, i.e. phase 2, the compiler inserted special code will come into action. Using the phase 1’s function signature, this special code access the classesv_table through the object’s v_ptr, finally getting the address of the correct function variant and invokes it through this address from v_table.
  7. Voila 🙂 Run time polymorphism has been achieved!

The presence of C++ keyword virtual tells the compiler that the function is polymorphic and will be overridden by some derived class.

Keyword override is optional. But as a C++ programmer I always recommend to use this keyword when you declare polymorphic functions in derived classes. This helps, trust me. The reason I am not explaining here, at this point. I am saving this topic for a later discussion.

Code to base class principle

When you write client code, i.e. code that uses the objects of a particular class, it’s difficult to keep track of how many derived classes are there and will get added. In such situations we write our code aligning to the principle of coding to a base class object. That means our client code will work on the assumption that the object is of the type base class. But when the client code runs, the correct functions will get invoked with the help of vptr and v_table that I mentioned above. This kind of polymorphism is possible in C++ only via pointers and references.

Let us walk through a portion of the above code to show this in work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ip_obejct is a reference to an object of the type Inetger_Pair 
void integer_pair_client(Integer_Pair &ip_object)
{
	// add is not a virtual function so this invokes base class add()
	cout << "\nip_object.add() : " << dec << ip_object.add() << endl;
	// divide is polymorphic, so this invokes derived calss divide 
	cout << "ip_object.divide() : " << ip_object.divide() << endl;
	
	return;
}

Focus to the following lines of the above result

ip_object.add() : 3
ip_object.divide() : 2

Client code function doesn’t even distinct between object instances of derived and base class. Invoking divide on ip_object calls the correct implementation of function through C++’s run time polymorphism I explained above. You understand, what and all happens under the hood now?

‘is a’ Relationship

Any derived class, also, is a base class in a class hierarchy. This is known as ‘is a’ relationship. That means Swapped_Pair object is also an Integer_Pair object.

NOTE 1: Always use references or pointers when using polymorphic objects, as an object of base class instance. Do not force type cast from derived object type to base object type, which will result in object trimming/truncation/slicing. Suppose derived class has declared extra data that are not declared in base class. Then what will happen if you typecast an object of that derived class to its base class? Then a bigger data type is getting type-casted to a smaller data type. If you know what will happen if you typecast int to a char or double to a float, you will understand what I am referring to. It will result in data loss.

NOTE 2: The other type of relationship is ‘has a’ which is related to composite class types, which I will explain later.

Static data members in C++ and static member functions

In simple terms, a static data member means there is only one copy of that data per class and no instances are further created per object. That also means static data members are not allocated within object memory, which implies, static data members of a class can be accessed by all of its objects, and all objects at any given point of time see the same static member. As static data members are common to any object instance or to the class itself, we can access such static data members either via object instance using ‘.’ (dot operator) or using the class name along with scope resolution operator ‘::’.

Static data declaration vs initialization

In C++, we need to note that, static data is just declared inside a class and is not instantiated. As you know when you just declare a class, and even if you define all member function bodies within the class itself, there is no data or object instantiation. So the question is when and how to instantiate a static data member. The obvious choice is to do this outside the class body.

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

using namespace std;

class Student
{
protected:
    static int count; // static is just for delcaring count.
    static const float record_version; // or use constexpr instead const for in-class initialization
    string name_;
    int marks_;
public:
    Student(const char *name, const int marks = 0): name_{name}, marks_{marks}
    {
        ++count;
        cout << "Student Record #" << dec << count << " for - " << name_ 
             << " with marks " << marks_ << " is created!" 
             << endl;
    }
    
    void set_marks(const int new_marks)
    {
        marks_ = new_marks;
    }
    
    void print_record(void) const // note the parameter list is marked as void
    {
        cout << "Student name - "  << this->name_ // this pointer still does its job :) 
             << " marks - " << dec << this->marks_ 
             << endl;
        
        return;
    }
    
    // static member function 
    static int get_current_count()
    {
        return count;
    }
    
    static float get_record_version()  
    {
        return record_version;
    }
    
    // overloaded version of set_marks function 
    static int set_marks(Student *this_student, const int marks)
    {
        this_student->marks_ = marks;
        cout << "Student - " << this_student->name_ 
             << " marks is updated to " << this_student->marks_
             << endl;
    }
    
    ~Student()  
    {
        --count;
    }    
};

// important step to instantiate and initialize static data of class 
int Student::count = 0; // note: dont add static keyword again here
const float Student::record_version = 1.2F;

Here, can you see how static data member ‘count’ is instantiate outside the class. But there are certain scenarios where instantiation and initialization has to be done inside the class itself. The candidates are constant static data members. But the problem with initializing values inside a class is permitted in C++ only for integral constants such as char, bool, int etc. and not applicable for floats, doubles and other user defined types. So how this is done for non-integral static constants types then? Again the answer is to do that outside the class or if you want to do that inside the class then instead of static ‘const’ float make it static constexpr float. Hope you have noticed this in the code snippet above.

Now coming to static member functions, such functions like any other non-static member functions are not allocated in an object’s memory. But these functions have an important aspect.

Have you ever wondered how non-static member functions can access an object’s private/protected/public data? These functions are implemented only once and are not aware of how many objects are instantiated? But these non-static member functions do know where the object on which the function is currently invoked is stored. This is achieved by C++’s this pointer. C++ compiler automatically adds this pointer to non-static member functions. But for static member functions, this is not done. This is because static member functions don’t work with object instances and rather they ONLY work with class itself. So there is no this pointer supplied by the C++ compiler to these functions.

NOTE 1: It doesn’t matter even if you mark parameter list to a non-static member function explicitly as void. The compiler will remove this and supply a ‘this pointer’ as the first parameter to all non-static member functions of a class.

NOTE 2:  None of the static member functions of a class will get access to ‘this pointer’ by the compiler. This doesn’t restrain a creative programmer in supplying one this pointer programmatically. The overloaded function set_marks declared as static shows how to do this elegantly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main()
{
    Student s1("Student_1", 95);
    s1.print_record();
    
    // See the magic of using a static member function 
	// to change student s1's marks
    Student::set_marks(&s1, 97);
    
    return 0;
}

The enum class of C++, operator overloading and friend functions: A use-case

For explaining this I have written a program illustrating a typical use case where a programmer can use the powerful C++ features such as enum class, friend functions and operator overloading.

The code extends Student base class to add data members such as standard, medium of instruction and affiliated syllabus for the curriculum.

Look at 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
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
class Student_Extended : public Student
{
public:
    enum class medium{english = 0, native = 1};
    enum class syllabus{center = 0, international = 1};
private:
    int standard_;
    medium med_;
    syllabus syl_;
    static const float record_version;
public:
    Student_Extended( const char *const name,
                      const int standard = 1, // default value is 1
                      const medium med = Student_Extended::medium::english, // default medium english
                      const syllabus syl = Student_Extended::syllabus::center ): // default syllabus center
        Student(name),    // calling base class constructor
        standard_{standard}, med_{med}, syl_{syl}// members are initialized
    {
        cout << "Student Record created with the details: \n"
             << "standard - " << dec << standard_  
             << ", in medium - " << med_ 
             << ", affiliated to " << syl_ << " syllabus"
             << endl;
    }
    
    static int get_current_count() 
    {
        return Student::count;
    }
    
    static float get_own_record_version()
    {
        return Student_Extended::record_version;
    }
    
    static float get_student_record_version()
    {
        return  Student::record_version;
    }
    
    // operator << has to be overloaded for enum medium and enum syllabus
    // need to use friend functions semantics here as << has to work on private class members 
    friend ostream& operator<<(ostream& this_ostream, const Student_Extended::medium& this_medium);
    friend ostream& operator<<(ostream& this_ostream, const Student_Extended::syllabus& this_syllabus);
    
};

const float Student_Extended::record_version = 5.7F;

ostream& operator << (ostream& this_ostream, const Student_Extended::medium& this_medium)
{
    switch(this_medium)
    {
        case (Student_Extended::medium::english): 
        {
            this_ostream << " English";
            break;
        }
        default:
        {
            this_ostream << " native";
            break;
        }
    }
    
    return this_ostream;
}

ostream& operator << (ostream& this_ostream, const Student_Extended::syllabus& this_syllabus)
{
    switch(this_syllabus)
    {
        case (Student_Extended::syllabus::center): 
        {
            this_ostream << " Center";
            break;
        }
        default:
        {
            this_ostream << " International";
            break;
        }
    }
    
    return this_ostream;
}

Now, let’s see how code is using Student_Extended below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main()
{  
    Student_Extended s2("Student_2"); // take other parameters as defaults 
    cout << "Number of student record(s) created : " << s2.get_current_count() << endl;
    
    cout << "Student Record Version : " << Student::get_record_version() << endl;
    cout << "Student Extended Record Version : " << Student_Extended::get_own_record_version() << endl;
    cout << "Student Base Record Version : " << Student_Extended::get_student_record_version() << endl;
    
    return 0;
} 

‘standard’ is just an ‘int’ not much merry about this. ‘medium’ and ‘syllabus’ are enum classes. To print these data properly and meaningfully we cannot use cout’s ‘<<’ put to output stream operator. We need to do two things here:

  1. Overload the operator ‘<<’ to work on enum classes medium and syllabus
  2. Make these overloaded operators friends to the class Student_Extended. This is required as overloaded operator function needs access to private/protected class data members. In the example I have written, ‘<<’ has to access med_ and syl, both of them are private data members of class Student_Extended.

NOTE: You may ask, why these overloaded operator functions can’t be made as class member functions? If they are member functions automatically the first parameter will be a reference to object instance (Remember the ‘this pointer’ ?). In the above example code, ‘<<’ operator is operating on operands of type medium and syllabus and not on type Student_Extended. Hope you understand the intricacies involved here.

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

+1