Move Constructors explained with simple example


In C++11 (Introduction to C++11) , move constructors are added which can improve any code’s performance drastically. Typically a move constructor is same as copy constructor which will create a new instance based on the passed object. But, the idea behind move constructor is to avoid memory reallocation and use as many memory from the passed original object because the original object is about to be deleted as it has been provided as a temporary object.

Since, in move constructors we avoid memory reallocation while creating new instance while will improve the code’s performance as we know memory allocation is quite costly.
While implementing move constructors we need to take care of one important point to ensure that the original object can be correctly destroyed.

Let’s take an example to see how this memory reallocation is avoided in case of move constructor.

#include <iostream>
#include <string.h>
#include <time.h>
#include <chrono>


using namespace std::chrono;
class protocol
{
    public:
        unsigned int version;
        unsigned int size;
        unsigned int headerSize;
        char *headerData;
        char *protocolData;
        char *headerData1;
        char *protocolData1;
        char *headerData2;
        char *protocolData2;
        char *headerData3;
        char *protocolData3;

    public:
        protocol(unsigned int version, unsigned int size, unsigned int headerSize, char *hData, char *pData): version(version),
                          size(size),
                          headerSize(headerSize)
        {
            std::cout<<"Inside constructor "<<strlen(hData)<<std::endl;
            headerData = new char[strlen(hData)];
            protocolData = new char[strlen(pData)];
            headerData1 = new char[strlen(hData)];
            protocolData1 = new char[strlen(pData)];

            headerData2 = new char[strlen(hData)];
            protocolData2 = new char[strlen(pData)];

            headerData3 = new char[strlen(hData)];
            protocolData3 = new char[strlen(pData)];
            
            std::copy(hData, (hData + strlen(hData)), headerData);
            std::copy(pData, (pData + strlen(pData)), protocolData);
            std::copy(hData, (hData + strlen(hData)), headerData1);
            std::copy(pData, (pData + strlen(pData)), protocolData1);
            std::copy(hData, (hData + strlen(hData)), headerData2);
            std::copy(pData, (pData + strlen(pData)), protocolData2);
            std::copy(hData, (hData + strlen(hData)), headerData3);
            std::copy(pData, (pData + strlen(pData)), protocolData3);


        }

        // copy constructor
        protocol(const protocol& copy): version(copy.version), size(copy.size), headerSize(copy.headerSize)
        {
            headerData = new char[strlen(copy.headerData)];
            protocolData = new char[strlen(copy.protocolData)];

            headerData1 = new char[strlen(copy.headerData1)];
            protocolData1 = new char[strlen(copy.protocolData1)];

            headerData2 = new char[strlen(copy.headerData2)];
            protocolData2 = new char[strlen(copy.protocolData2)];

            headerData3 = new char[strlen(copy.headerData3)];
            protocolData3 = new char[strlen(copy.protocolData3)];

            std::copy(copy.headerData2, (copy.headerData2 + strlen(copy.headerData2)), headerData2);
            std::copy(copy.protocolData2, (copy.protocolData2 + strlen(copy.protocolData2)), protocolData2);
            std::copy(copy.headerData1, (copy.headerData1 + strlen(copy.headerData1)), headerData1);
            std::copy(copy.protocolData1, (copy.protocolData1 + strlen(copy.protocolData1)), protocolData1);
            std::copy(copy.headerData, (copy.headerData + strlen(copy.headerData)), headerData);
            std::copy(copy.protocolData, (copy.protocolData + strlen(copy.protocolData)), protocolData);
            std::copy(copy.headerData3, (copy.headerData3 + strlen(copy.headerData3)), headerData3);
            std::copy(copy.protocolData3, (copy.protocolData3 + strlen(copy.protocolData3)), protocolData3);

            std::cout<<"Inside copy constructor "<<strlen(copy.headerData) <<std::endl;
        }
        
        //move constructor
        protocol(protocol&& copy): version(copy.version), size(copy.size), headerSize(copy.headerSize)
        {
            headerData = copy.headerData;
            protocolData = copy.protocolData;
            copy.protocolData = NULL;
            copy.headerData = NULL;

            headerData1 = copy.headerData1;
            protocolData1 = copy.protocolData1;
            copy.protocolData1 = NULL;
            copy.headerData1 = NULL;

            headerData2 = copy.headerData2;
            protocolData2 = copy.protocolData2;
            copy.protocolData2 = NULL;
            copy.headerData2 = NULL;

            headerData3 = copy.headerData3;
            protocolData3 = copy.protocolData3;
            copy.protocolData3 = NULL;
            copy.headerData3 = NULL;

            std::cout<<"Inside move constructor "<<strlen(headerData) <<std::endl;
        }

        ~protocol()
        {
            std::cout<<"Inside destructor "<<std::endl;
            delete[] headerData;
            delete[] protocolData;
            delete[] headerData1;
            delete[] protocolData1;
            delete[] headerData2;
            delete[] protocolData2;
            delete[] headerData3;
            delete[] protocolData3;
        }
};

int main()
{
    char *hd = "headerdatais123i666666666666666666666666666666666";
    char *pd = "protocoldatais99999999999999999988776655667788997";

    high_resolution_clock::time_point t1 = high_resolution_clock::now();
    protocol p(1,200,strlen(hd),hd,pd); //Normal constructor

    high_resolution_clock::time_point t2 = high_resolution_clock::now();
    protocol p1(*(new protocol(1,200,strlen(hd),hd,pd))); //Copy constructor
    
    high_resolution_clock::time_point t3 = high_resolution_clock::now();
    protocol p2(std::move(*(new protocol(1,200,strlen(hd),hd,pd)))); // Move Constructor
    
    high_resolution_clock::time_point t4 = high_resolution_clock::now();

    auto duration1 = duration_cast<microseconds>( t2 - t1 ).count();
    auto duration2 = duration_cast<microseconds>( t3 - t2 ).count();
    auto duration3 = duration_cast<microseconds>( t4 - t3 ).count();
    
    std::cout<<"Object created via Normal constructor execution time: "<<duration1<<std::endl;
    std::cout<<"Object created via Copy constructor execution time: "<<duration2<<std::endl;
    std::cout<<"Object created via Move constructor execution time: "<<duration3<<std::endl;
    
    return 0;
}

As shown in above example, copy constructor is doing deep copy where as move constructor is simply stole the memory from passed object to avoid reallocation of memory.

Execution time of these constructors will confirm that move constructor is indeed provide better performance.

Output of above program:

Run 1:

Inside constructor 49
Inside constructor 49
Inside copy constructor 49
Inside constructor 49
Inside move constructor 49
Object created via Normal constructor execution time: 10
Object created via Copy constructor execution time: 31
Object created via Move constructor execution time: 17
Inside destructor 
Inside destructor 
Inside destructor 

Run 2:

Inside constructor 49
Inside constructor 49
Inside copy constructor 49
Inside constructor 49
Inside move constructor 49
Object created via Normal constructor execution time: 11
Object created via Copy constructor execution time: 22
Object created via Move constructor execution time: 15
Inside destructor 
Inside destructor 
Inside destructor

As we can see in both example move constructor is almost 30-50% faster than normal copy constructor.

Few Important points:

  • Move constructor must be ensured to call from temporary object which is going to be destroyed. If passed object is used somehow later in the code then this object will have inconsistent data that can lead to errors/crash etc. For eg:
    In above code if in place of following line:
protocol p2(std::move(*(new protocol(1,200,strlen(hd),hd,pd)))); 

if we call it like as follows:

protocol p2(std::move(p1)); 

Then it will move p1 member pointers to NULL. As this p1 is still valid after this operation, access to this object will lead to incorrect data which can crash the system too. Hence, extra precautions needs to be maintained.

  • In case move constructor is not defined in the class then copy constructor will be called automatically by the compiler. compiler will not throw any error for this.
  • Move constructors can be called only with rvalue references. If anybody wants to call move constructor with an lvalue then “std::move” needs to be used as shown in above example.
    std::move (for more details click here) is a compiler provided function which changes any lvalue to rvalue to ensure move constructors will be called.

Move constructors and copy constructors both are kind of same. For more details related to move constructors vs copy constructors click here.

 

Leave a Reply

Your email address will not be published. Required fields are marked *