Move Semantics

In this post we are going to take a look at move semantics. Move semantics rely on the concept of rvalues, which were introduced in C++11. First we must make the distinction between lvalues and rvalues.

lvalues are called as such because they have traditionally appeared on the left hand side of an assignment operator and designate an object (among other things).

rvalues are called as such because they have traditionally appeared on the right hand side of an operator and typically designate temporary objects (possibly other things).

How do we refer to such things in C++? Like so...
X&      // lvalue reference
X&&    // rvalue reference
What are the uses of such things? One of the main ramifications of rvalue references is move semantics. What this allows us to do is create objects from temporary objects without having to make a deep copy.
#include<iostream>
#include<list>

class BigVec
{
private:
    int         m_size;
    double*     m_data;
public:
    // copy constructor
    BigVec(const BigVec& bv) : m_size(bv.m_size) {
        m_data = new double[bv.m_size];
        for(int i=0; i<m_size; i++) { m_data[i] = bv.m_data[i]; }
    }
    
    // copy assignment operator
    BigVec& operator=(BigVec& bv) {
        m_data = new double[bv.m_size];
        for(int i=0; i<m_size; i++) { m_data[i] = bv.m_data[i]; }
        return *this;
    }
    
    // move constructor
    BigVec(BigVec&& bv) : m_size(bv.m_size),
    m_data(bv.m_data) {
        bv.m_data = nullptr;
    }
    
    // move assignment operator
    BigVec& operator=(BigVec&& bv) {
        if(this == &bv) return *this; // is self referential ?
        delete m_data;                // delete current data
        m_data = bv.m_data;
        m_size = bv.m_size;
        // reset object as resources have been moved
        bv.m_data = nullptr;
        bv.m_size = 0;
        return *this;
    }
    
    // toy constructor
    BigVec(int size) : m_size(size) {
        m_data = new double[size];
        for(int i = 0; i < size; i++) m_data[i] = 0;
    }
    
    int getSize() { return m_size; }
    
    ~BigVec() { delete m_data; }
};

BigVec CreateBigVec() {
    return BigVec(100);
}

int main()
{
    // Copy constructor is called
    BigVec v = CreateBigVec();
    BigVec vec(v);
    
    // Move constructor is called
    std::list<BigVec> listOfBigVecs;
    listOfBigVecs.push_back(CreateBigVec());

    // Move assignment operator is called
    vec = BigVec(10);
    
    // Calls the toy constructor
    BigVec vec1 = BigVec(1);
    // this is the same as
    BigVec vec2(1);
    
    std::cout << vec1.getSize() << std::endl;
    return 0;
}
The example above shows how the different constructors are called. Perhaps the most significant is the move constructor. We can execute the following code, which will fill our list with BigVec's. Each time we add an item to the list the move constructor is being called.
for(int i = 100; i < 1000; i++) {
    listOfBigVecs.push_back(BigVec(i));
}
The move constructor requires a fixed number of operations per call, and so the above code is linear in time complexity.
for(int i = 100; i < 1000; i++) {
    BigVec temp(i);
    listOfBigVecs.push_back(temp);
}
The above code would require 899 calls to the copy constructor, which takes linear time. This immediately makes our code quadratic in time complexity.

We can make use of std::move which will essentially allow us to treat created objects as temporary ones. More precisely, std::move is a cast that produces an rvalue-reference to and object to enable moving from it. Although we have to keep in mind that we can no longer say that the passed object is non-empty. For example,
    auto v = CreateBigVec();
    BigVec vec1(v);                 // copy
    BigVec vec2(std::move(v));      // move

Comments

Popular posts from this blog

Using the AVX instruction set to boost performance

The magic behind deep learning : Obtaining gradients using reverse Automatic Differentiation.

Information Entropy