Operator overloading is one of the fundamental operation which come across often in a C++ program. It is bit cryptic in syntactical side as well as often a misunderstood topic amongst new programmer. I will try to explain this as a series of C++ related notes (as I like to call this) following this post.
Okay, so to start with lets consider the following piece of code:
#include <iostream>
#include <string.h>
class Packet {
private:
char *buf{nullptr};
static const int max_buf_size{256};
public:
Packet() {}
Packet(const char *data, size_t n)
{
if(!buf) {
buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
memcpy(buf, data, n < max_buf_size ? n : max_buf_size);
}
void print_bytes(size_t n)
{
for(int i=0; i < n; ++i)
std::cout << buf[i] << " ";
std::cout << std::endl;
}
};
int main()
{
char *dummy_data = new char[26];
for(unsigned int i=0; i < 26; ++i)
dummy_data[i] = 'a' + i;
Packet p1(dummy_data, 26);
p1.print_bytes(26);
return 0;
}
The above program can be downloaded from here as well.
Note: to download the complete source code please click here.
Now, in the above program we have constructed a class called Packet, which simply contains a pointer to a consecutive bytes in memory often called as a buffer, in this example ‘buf‘. The main function of the Packet class is to hold a small data in its buf structure.
There is a no parameter constructor of the class which is just a place holder and there is a two parameter constructor of the class which essentially takes a data pointer and the size of the data. There is a print_bytes function which helps to print the content of the data. The max buf size is determined by a static member of the class called ‘max_buf_size‘.
Now in the main we have created an instance of the Packet class by passing a dummy_data and its size. At this point the two-parameter constructor is called and it checks if the buffer point ‘buf‘ has been initialised or not. If not already initialised the constructor allocates the buffer with maximum possible size (to keep things simple) and then clears up the allocated memory with memset call. Next the constructor copies the data into the buffer by calling memcpy. I will not go into the details of any of the library function used here as that is out of the scope of this discussion. Then the buffer content is printed with a call to print_bytes() member function call.
Now if we compile the program and run it we will see something like the below (I am running it on CodeBlocks in my Windows machine):

Now lets modify the code to add the below lines to create another instance of Packet class and then try assigning the values of the first instance to the newly created instance as below:
Packet p2;
p2 = p1;
p2.print_bytes(26);
Now in the above code snippet we have instantiated a new instance of Packet class called ‘p2’ simply by calling the no-parameter constructor and then have assigned the instance ‘p1’ to it. Then we have called the print_bytes() member function on the p2 instance. The output looks somewhat like the below in my Windows PC in CodeBlocks:

As you can see all the bytes of the buffer ‘buf’ are being printed for both the instances. So good so far.
Now lets say this is large program and during the course of the program we need to release the buffer held by the instance p1 as it is not required. The reason for this may not be quite evident in this example as it is meant to be ridiculously simply for discussion purposes. But think about a real life program with thousands of different scenarios where a packet manager is supposed to create packets and then time to time release the packets as those expires. So ultimately the time has arrived for our dear packet p1 to depart. For releasing a buffer area we will add a utility member function in the class definition and then call it in due course. Please notice the below changes to the program (the whole program may be downloaded from here):
class Packet {
private:
char *buf{nullptr};
static const int max_buf_size{256};
public:
Packet() {}
Packet(const char *data, size_t n)
{
if(!buf) {
buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
memcpy(buf, data, n < max_buf_size ? n : max_buf_size);
}
void print_bytes(size_t n)
{
for(int i=0; i < n; ++i)
std::cout << buf[i] << " ";
std::cout << std::endl;
}
void release_buffer(void)
{
delete buf;
}
};
The above code has introduced the new addition of the utility function release_buffer() in the Packet class definition. Now in the main we add the below lines of code:
int main()
{
char *dummy_data = new char[26];
for(unsigned int i=0; i < 26; ++i)
dummy_data[i] = 'a' + i;
Packet p1(dummy_data, 26);
p1.print_bytes(26);
Packet p2;
p2 = p1;
p1.release_buffer();
p2.print_bytes(26);
return 0;
}
Now if we compile and run the program again, we will see something like this:

If you look at the above closely you will see the first print is looking ok but in case of the second print there are lots of junk characters been printed. That is strange, is it not? Actually what has happened here is just after calling the release_buffer() member function of the p2 instance the buffer ‘buf’ has been erased from the memory and that has caused a lot of grief for the p1. But how? This is classically known as a side effect of shallow copy by the C++ runtime.
Shallow copy and dangling pointer
Look at the below operation:
p2 = p1;
In the above the assignment operator is being invoked to copy the content of the p1 instance to the p2 instance. But how does the operator copies the content of the one instance to other? The usual assignment operator is extended to carry out its duty of copying class member variables across object by the compiler which is known as operator overloading. The very use of the word overloading might already be giving you a hint that there is something extra duty being executed by the operator. Now, compilers do not know the intricacies of your class nor it knows or supposed to know how the class fits into the bigger scheme of things in your larger program. Hence, the overloaded assignment operator provided by the compiler is pretty rudimentary. All it does is a member to member copy of the class variables. Now this works fine until there is no external entity involved in the process. Had all the class members been C++ fundamental data types like int or char or double etc there won’t have been any issue. But in the cases of a pointer being the member of a class there is an external entity involved in this scenario which is the piece of memory the pointer is pointing to. Compiler doesn’t know about it nor it cares. So what it does is takes the memory address kept in the buf member of the p1 instance and stores it into the buf member of the p2 instance. The scenario is somewhat like the below diagram:

Now till the p1 instance doesn’t release the buf or modifies buf, there should be no apparent problem, or at least won’t be evident. But the moment p1 modifies the data in the buf, there will be inconsistency in p2 which it doesn’t have any idea of. The worst comes when the buf is released by the p1 instance. At that point buf in p2 is holding a memory address which has been freed and perhaps been allocated to some other piece of code for some other purposes. At this point the buf point in p2 becomes a dangling pointer, a pointer which points to an undefined memory location or an invalid or illegal memory location. That’s why we see garbage been printed in the print_bytes function when called by the p2 instance. If this program continues soon the program will be caught by a null pointer exception and the whole program falls apart.
Overloading the assignment operator
The simple solution to this problem is to not rely on the compiler provided rudimentary overloaded assignment operator rather rewrite it ourselves as per our need. And ours would be more sophisticated than the compiler’s, duh!
Syntax
The syntax of the overloaded assignment is bit cumbersome at least I find it bit a memory jog. Lets talk about the first thing we have to deal here that is the return type of the overloaded assignment operator. If you think about it what all the overloaded assignment operator is doing is copying one object into another. So both the operands are of the actual class type which is Packet in this case. Hence, as a memory tip always remember the outcome of the operator is also the class type i.e. in this case Packet class type. One more thing, once the overloaded operator has done its job i.e. forming an object of the class type by copying content from the supplied object, it must not be modified accidentally. Hence, const is our friend here. The return type has to be const type. One more C++ memory tip is to think about the copying one object to another as a memory to meory copy operation and to reduce the amount of copying we would have to use a reference. So all these put together the return type of the overloaded operator will be a const Packet type reference, i.e. const Packet &.
Now, for any operator overloading we have an keyword to remember and that is operator immediately followed by the operator being overloaded, in this case ‘=’. Hence, the next piece of the puzzle is operator=. So fat the first line of our endeavour to write the overloaded assignment operator looks like this:
const Packet& operator=
Now coming to the arguments. The arguments to the overloaded assignment operator are two, the first is an implicit argument which is this pointer. It points to the object which resides on the left hand side of the operator i.e. in our case p2. There is no need pass this as an argument as C++ compiler does this for us silently. The second argument is the one residing on the right hand side of the = operator i.e. in our case p1. Now again memory tip, when we pass p1 as an argument to = operator we do not want it to modify the p1 instance even by accident. Hence, we would qualify it by const and to reduce memory copying we would be suing a reference to p1 rather than passing it by value. So till now our overloaded operator looks like this:
const Packet& operator=(const Packet& other)
Then the rest is the body of the overloaded operator. In the body we want to make sure memory is properly allocated for the buf instance. So we check if there has been any memory allocated already or not bu null pointer check and if not then allocate the memory. So far the overloaded assignment operator looks like this:
const Packet& operator=(const Packet& other)
{
if(!buf) {
buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
}
As you can see above after allocating the memory we have cleared it up with memset which is a standard practice to ensure there is no garbage content in the memory while being allocated form the heap. Next we copy the buf content from the p1 instance whcih is denoted by the passed reference other in our case. So the code now looks like this:
const Packet& operator=(const Packet& other)
{
if(!buf) {
buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
memcpy(buf, other.buf, max_buf_size);
}
Remember you could have used the this pointer to explicitly denote the member variable buf in this, that would be completely legal but still redundant as the compiler already does that for you. So you could have written the code as below:
const Packet& operator=(const Packet& other)
{
if(!this->buf) {
this->buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
memcpy(this->buf, other.buf, max_buf_size);
}
It is not required for any practical purposes but it emphasises the fact that the this pointer is the owner of the memory where buf resides. And also, it perhaps helps to remember the what to be returned in the last statement in the overloaded operator. In the last statement we return *this which simplistically means the content of the memory location i.e. p2 instance and finally a reference is returned to the caller. So the complete overloaded assignment operator now looks like below:
const Packet& operator=(const Packet& other)
{
if(!this->buf) {
this->buf = new char[max_buf_size];
memset(this->buf, 0, max_buf_size);
}
memcpy(this->buf, other.buf, max_buf_size);
return *this;
}
Or for all practical purposes as below:
const Packet& operator=(const Packet& other)
{
if(!buf) {
buf = new char[max_buf_size];
memset(buf, 0, max_buf_size);
}
memcpy(buf, other.buf, max_buf_size);
return *this;
}
The output looks like the below in my Windows PC:

Now, time for another C++ terminology which is deep copy. The above way of copying the content of once class instance to another is called deep copy i.e. not one to one copying the member variables but to actually taking care of any inherent memory or resource allocation and then carrying out the copy operation.
The complete source code may be downloaded from here.
That’s all for this post, have fun!
Leave a Reply