C++: Move semantics

This example involves a class A and a container called List<T>. As shown, the container is essentially a wrapper around std::vector.

There is also a function called getNObjects that returns a list containing N instances of class A.

#include <vector>
#include <string>
#include <iostream>
 
class A
{
public:
    A() = default;
    ~A() = default;
    A(const A& a) { std::cout << "copy ctor" << std::endl ; }
     
    A& operator=(const A&)
    {
        std::cout << "operator=" << std::endl;
        return *this;
    }
};
 
template <typename T>
class List
{
private:
    std::vector<T>* _vec;
     
public:
    List() : _vec(new vector<T>()) { }
    ~List() { delete _vec; }
     
    List(const List<T>& list)
        : _vec(new vector<T>(*(list._vec)))
    {
    }
     
    List<T>& operator=(const List<T>& list)
    {
        delete _vec;
        _vec = new vector<T>(*(list._vec));
        return *this;
    }
     
    void add(const T& a)
    {
        _vec->push_back(a);
    }
     
    int getCount() const
    {
        return static_cast<int>(_vec->size());
    }
     
    const T& operator[](int i) const
    {
        return (*_vec)[i];
    }
};
  
List<A> getNObjects(int n)
{
    List<A> list;
    A a;
    for (int i = 0; i< n; i++)
        list.add(a);
 
    std::cout << "Before returning: ********" << std::endl;
    return list;
}
 
int main()
{
    List<A> list1;
    list1 = getNObjects(10);    
    return 0;
}

When this code runs, it will produce an output like this:

...
...
copy ctor
copy ctor
Before returning: ********
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor
copy ctor

The number of calls to the copy constructor equals the number of objects in the list!

Why?

Because when the getNObjects() function returns a list, all its attributes are copied (i.e., the internal vector is copied) to the list that receives the result (list1), and then the local list inside the function is destroyed (triggering the destructor for each object in the list). Though this is logically correct, it results in poor performance due to many unnecessary copies and destructions.

Starting from C++11, a new type of reference is available to address this problem: rvalue references. An rvalue reference binds to a temporary object (rvalue), which is typically the result of an expression that is not bound to a variable. Rvalue references are denoted using the symbol &&.

With rvalue references, programmers can create move constructors and move assignment operators, which improve performance when returning or copying objects in cases like this example.

How does this work?

In the example, the List<T> class contains a pointer to a vector. What happens if, instead of copying every object in the std::vector, the programmers “move” the vector pointer from the local list inside the function to the list that receives the result? This would save a lot of processing time by avoiding unnecessary copies and destructions.

Thus, the move constructor and move assignment operator work as follows: They receive an rvalue reference to the list being moved, “steal” its data, and take ownership of it. Taking ownership means that the object receiving the data is responsible for releasing all resources originally managed by the moved-from object (achieved by setting the original object’s pointer to nullptr).

Here’s how imove constructor and move assignment operator can be implemented for the List<T> class:

List(List<T>&& list) //move constructor
    : _vec(list._vec)
{
    list._vec = nullptr; //releasing ownership
}
     
List<T>& operator=(List<T>&& list)
{
    delete _vec;
    _vec = list._vec;
    list._vec = nullptr; //releasing ownership
    return *this;
}

With these changes, the output would be:

...
...
copy ctor
copy ctor
Before returning: ********

All the copy constructor calls after the “Before returning” line are avoided.

Isn’t that great?

What other uses do rvalue references have?

Here’s an example of a simple swap function for integers:

void my_swap(int& a, int& b)
{
  int c = a;
  a = b;
  b = c;
}

Straightforward enough. But what if, instead of swapping two integers, we needed to swap two large objects (such as vectors, linked lists, or other complex types)?

template <typename T>
void my_swap(T& a, T& b)
{
  T c = a;
  a = b;
  b = c;
}

If the copy constructor of class T is slow (like the std::vector copy constructor), this version of my_swap can have very poor performance.

Here’s an example demonstrating the issue:

#include <iostream>
#include <string>
  
class B
{
    private: int _x;
    public:
        B(int x) : _x(x) { cout << "ctor" << endl; }
         
        B(const B& b) : _x(b._x)
        {
            std::cout << "copy ctor" << std::endl;
        }
         
        B& operator=(const B& b)
        {
            _x = b._x;
            std::cout << "operator=" << std::endl;
            return *this;
        }
 
        friend std::ostream& operator<<(std::ostream& os, const B& b)
        {
            os << b._x;
            return os;
        }
         
};
 
template <typename T>
void my_swap(T& a, T& b)
{
    T c = a; //copy ctor, possibly slow
    a = b;   //operator=, possibly slow
    b = c;   //operator=, possibly slow
}
 
int main()
{
    B a(1);
    B b(2);
    my_swap(a, b);
    std::cout << a << "; " << b << std::endl;
    return 0;
}

The output is:

ctor
ctor
copy ctor
operator=
operator=
2; 1

The class B is simple, but if the copy constructor and assignment operator are slow, my_swap‘s performance will suffer.

To add move semantics to class B, move constructor and move assignment operator must be implemented:

B(B&& b)  : _x(b._x)
{
    std::cout << "move ctor" << std::endl;
}
         
B& operator=(B&& b)
{
    _x = b._x;
    std::cout << "move operator=" << std::endl;
    return *this;
}

However, the move constructor and assignment operator will not be invoked automatically. In my_swap, the compiler does not know if it should use the copy or move versions of the constructors and assignment operators.

This problem can be fixed by explicitly telling the compiler to use move semantics using the function template std::move():

template <typename T>
void my_swap(T& a, T& b)
{
    T c = std::move(a); //move ctor, fast
    a = std::move(b);   //move operator=, fast
    b = std::move(c);   //move operator=, fast
}

The std::move function casts an lvalue to an rvalue reference, signaling to the compiler that it should use the move constructor and assignment operator.

The updated output is:

ctor
ctor
move ctor
move operator=
move operator=
2; 1

The entire standard library has been updated to support move semantics.

Perfect forwarding is another feature built on top of rvalue references.

3 thoughts on “C++: Move semantics

  1. Just desire to say your article is as amazing. The clarity in your post is just spectacular and i could assume you’re an expert on this subject. Fine with your permission let me to grab your RSS feed to keep up to date with forthcoming post. Thanks a million and please continue the enjoyable work.

Leave a reply to Su Cancel reply