C++ 11: Emplacement vs Insertion for STL Containers

Discussion in 'C++' started by BiplabKamal, Apr 20, 2016.

  1. Invoking copy constructor in C++ has always been problematic, specially when the copy operation is expensive. While passing parameter by value and the invocation of copy constructor can impose huge performance penalty specially when temporary objects are created, copied to another object and the destroying the temporary object. Language like C# does not have copy constructor and objects are reference type. C++ objects are value types and calls copy constructor when you pass object as argument by value, Return objects by value and initialize a new object with an existing object. If we does not know the implication we can land to unnecessary creation of duplicate objects and reduce the performance. For example look at the following code. It has a class implementing the copy constructor, move constructor, constructor and destructor to show when and how many times they are called. Also an instance count is maintained to track the number of instances at any point of time.

    Code:
    #include<vector>
    #include<iostream>
    using namespace std;
    class MyClass
    {
    private:
        static int instancecount;//This is used to track the instance count
        int *p;
        int id;
    public:
        MyClass(int n):p{nullptr},id{n}
        {
            p = new int(100);
            cout << "New instance of MyClass is created" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        //Copy constructor will accept lvalue reference. and also rvalue as due to constness of the lvalue reference
        MyClass(const MyClass& obj)
        {
            p = new int(0);
            *p = *obj.p;
            id = obj.id;
            cout << "New instance of MyClass is created with the copy of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        // Move constructor will only accept rvalue object
        MyClass(MyClass&& obj)
        {
            p = obj.p;
            obj.p = nullptr;
            id = obj.id;
            cout << "New instance of MyClass is created with the move of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        ~MyClass()
        {
            if (p != nullptr)
                delete p;
            cout << "One instance of MyClass is destroyed" << endl;
            cout << "Total Number of instances: " << --instancecount << endl << endl;
        }
    };
    int MyClass::instancecount = 0;
    
    int main()
    {
        vector<MyClass> v;
        v.push_back(10);
        
        return 0;
    }
    
    The output will display:
    Code:
    New instance of MyClass is created
    Total Number of instances: 1
    
    New instance of MyClass is created with the move of another instance
    Total Number of instances: 2
    
    One instance of MyClass is destroyed
    Total Number of instances: 1
    
    One instance of MyClass is destroyed
    Total Number of instances: 0
     
    The output of the above code shows that two objects are created, constructor called twice(constructor and move constructor), destructor called twice. Why two objects are created when our intention is to create one object and place that in the vector. What is happening in the above code is that when you call v.push_back(10); you are not passing the matching argument of type MyClass. The push_back() method has two versions as below:
    Code:
    void push_back(const MyClass&); // takes lvalue object or rvalue object
    void push_back( MyClass&&); // takes only rvalue object
    
    In the above program when the compiler does not find the matching parameter it generates code to create a temporary instance of MyClass with the parameter passed to push_back() method and then call the rvalue version. The the statement in the main function become similar to
    Code:
        vector<MyClass> v;
        v.push_back(MyClass(10));
    
    Now what the vector’s push_back() method requires to create a copy of the object and place it in the list.

    But This redundant creation of temporary object is not intended. If the object is huge and copying is costly both in terms of space and time and you have hundreds of objects to insert, the performance penalty is going to be huge. After all C++ programs are most of the time performance sensitive.

    C++11 move semantics has tried to minimize the impact of copy when objects are copied from a temporary object. Move semantics move the resources of the object from source to destination instead of copy when the source object is a temporary object. It does not stop creating temporary object. Traditional implementation of STL containers have the problem of creating unnecessary instances of objects when inserting elements. Now C++11 has given a new method std::vector::emplace_back() to solve the issue. Let us first try to use it. Using the same class the main function will look like
    Code:
    int main()
    {
        vector<MyClass> v;
        v.emplace_back(10);
        return 0;
    }
    
    and output is:
    Code:
    New instance of MyClass is created
    Total Number of instances: 1
    One instance of MyClass is destroyed
    Total Number of instances: 0
    
    In the above code I used only one emplace_back call. In VS 2015 if if I try to do several emplacement it will again create lot of temporary objects. This is because for every insertion the vector reallocates the space to accommodate the new element. It will create temporary objects and move the existing elements to temporary objects and reallocate the memory. This is because the vector does not allocate extra buffer in advance. So every time it needs to reallocate the memory when placing a new object in the array. I am not sure about this peculiar behavior. You can get rid of this problem by calling vector::reserve() method before pushing any element. reserve(n) function will allocate the memory needed to store n number of elements.

    std::vector::emplace_back() method creates the object in side the vector instead of copying from the temporary object. Most of the STL containers supporting insertion functions also support emplacement functions. Alternative to push_back() is emplace_back(), alternative to push_front() is emplace_front() and alternative to insert() is emplace(). Emplacement functions are variadic template functions with parameter pack. The parameter pack is forwarded to the constructor of the element to be placed in the container. emplce() function is used to insert an element in a given position, so the iterator is given as the input position. Emplacement functions use parameter packs and perfect forwarding to forward the arguments to the constructor of the element to be created. Here are the examples of other emplacement functions using the same class above:

    Code:
    #include<vector>
    #include<map>
    #include<queue>
    #include<list>
    #include<iostream>
    
    using namespace std;
    class MyClass
    {
    private:
        static int instancecount;//This is used to track the instance count
        int *p;
        int id;
    public:
        MyClass(int n,int extra):p{nullptr},id{n}
        {
            p = new int(100);
            cout << "New instance of MyClass is created" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        //Copy constructor will accept lvalue reference. and also rvalue as due to constness of the lvalue reference
        MyClass(const MyClass& obj)
        {
            p = new int(0);
            *p = *obj.p;
            id = obj.id;
            cout << "New instance of MyClass is created with the copy of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        // Move constructor will only accept rvalue object
        MyClass(MyClass&& obj)
        {
            p = obj.p;
            obj.p = nullptr;
            id = obj.id;
            cout << "New instance of MyClass is created with the move of another instance" << endl;
            cout << "Total Number of instances: " << ++instancecount << endl << endl;
        }
        //Copy assignment operator will accept both lvalue and rvalue as argument
        MyClass& operator=(const MyClass& source)
        {
            if (this != &source)
            {
                *p = *source.p;
                id = source.id;
                cout << "Existing instance of MyClass is being overwriten with the copy assignment" << endl<<endl;
    
            }
            return *this;
        }
    
        // Move assignment operator will accept only rvalue argument
        MyClass& operator=(MyClass&& source)
        {
            if (this != &source)
            {
                if(p != nullptr)
                    delete p; // releasing old memory because size of new memory may be different
                
                p = source.p;
                id = source.id;
                source.p = nullptr;
                cout << "Existing instance of MyClass is being overwriten with the move assignment" << endl<<endl;
    
            }
            return *this;
        }
        ~MyClass()
        {
            if (p != nullptr)
                delete p;
            cout << "One instance of MyClass is destroyed" << endl;
            cout << "Total Number of instances: " << --instancecount << endl << endl;
        }
    };
    int MyClass::instancecount = 0;
    
    int main()
    {
        
        list<MyClass> mylist;
        mylist.emplace_front(10, 0);
        map<int, MyClass> mymap;
        mymap.emplace(2,MyClass(10,0));
        return 0;
    }
    
    All the compilers may not implement emplacement functions as intended. But that does not make emplacement functions less efficient than insertion functions. So no harm in using them. On the other hand STL containers are complex objects, so you should know the implication when you are using them. It may not be always wise to use emplacement functions instead of insertion function. It is better to benchmark both version before selecting the better one.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice