Move Semantics

Move Semantics

Introduction

  • When: C++11
  • Context: Arrived with Smart Pointers

Move Semantics are a key feature that improve performance and add a more modern view of C++. We can move rather than copying resources (or objects) which are expensive to copy.

To do so -for classes or structures- we need a move constructor and assignment operator.

But what is the concept of Move Semantics?

The problem

Let’s say that we want have to assign the content of a char * attribute from an Instance B of MyClass class to an Instance A.

By default, it will be a copy. Here the steps:

  1. Instance A will delete the content of its char *
  2. A new allocation is necessary to welcome the data that will be transmitted by Instance B
  3. Then data are copied from Instance B to Instance A

Sure it doesn’t take to much resources for one case. But imagine that we have hundreds of variables that we want to transfer some data (that can take a lot of place in the heap)!

C++11 brought the concept of rvalue that can solve this problem.

lvalue and rvalue

Here the definition :

An lvalue is an expression e that may appear on the left or on the right hand side of an assignment, whereas an rvalue is an expression that can only appear on the right hand side of an assignment. A rvalue reference is usually used to specify a reference to an object that is going to be destroyed.

Here an example:

int a = 42;
int b = 43;

// a and b are both lvalues:
a = b; // ok
b = a; // ok
a = a * b; // ok

// The expression 'a * b' is an rvalue:
int c = a * b; // ok, rvalue on right hand side of assignment
a * b = 42; // error, rvalue on left hand side of assignment

The object created by a * b has a short life. Once it’s created, the result is copied into the c variable then destroyed. Thus, there is a waste of memory.

Another important point, it is possible to get a reference to an rvalue:

  • int& means that we get the reference of an int. We can also call that getting the lvalue reference.
  • int&& means that we get the rvalue reference.

An example:

void print(int& x); // lvalue reference overload
void print(int&& x); // rvalue reference overload

int x;
int getAnInteger();

print(x); // argument is lvalue: calls print(X&)
print(getAnInteger()); // argument is rvalue: calls print(X&&)

The compiler will detect which function to call. We can apply the rvalue reference to any method or functions.

Note:

void foo(int&& t)
{
  // t is initialized with an rvalue expression
  // but is actually an lvalue expression itself
}

Move constructor and move assignment

Since C++11, there is new kind of constructor and assignment operator available. They can take rvalue references. That means instead of copying data, we can move them. As we want to move data from an instance to another, we must set our parameters as non-const. Example:

foo(const foo& p_other); // Copy constructor
foo(foo&& p_other); // Move constructor

foo& operator=(const foo& p_other); // Copy assignment
foo& operator=(foo&& p_other); // Move assignment

Here a second example :

#include <iostream>
#include <utility>

class foo
{
public:
    foo()
    {
        std::cout << "Default constructor" << std::endl;
    }

    foo(const foo& p_other)
    {
        std::cout << "Copy constructor" << std::endl;
    }

    foo(foo&& p_other) noexcept
    {
        std::cout << "Move constructor" << std::endl;
    }

    foo& operator=(const foo& p_other)
    {
        std::cout << "Copy assignment" << std::endl;
        return *this;
    }

    foo& operator=(foo&& p_other) noexcept
    {
        std::cout << "Move assignment" << std::endl;
        return *this;
    }
};

int main()
{
    foo originalFoo{};
    foo copyConstructor{ originalFoo };
    foo moveConstructor{ std::move(originalFoo) };

    foo copyAssignment;
    copyAssignment = originalFoo;

    foo moveAssignment;
    moveAssignment = std::move(originalFoo);
}

The output should be:

Copy constructor
Move constructor
Copy assignment
Move assignment

Transfering data

To apply the transfer of data we call the std::move(). It will trigger the move constructor or the move assignment.

Remember the diagram of copying data from an Instance B of MyClass to an Instance A of MyClass ? To avoid the waste of memory, we could move the data from one instance to another without allocating new memory.

Here an example :

#include <iostream>
#include <utility>

class MyClass
{
private:
    int size;
    char* data;
public:
    MyClass(int p_size) : size(p_size), data(new char[size])
    {
        std::cout << "Default constructor" << std::endl;
    }

    ~MyClass() { delete[] data; }

    MyClass(const MyClass& p_other) : size(p_other.size)
    {
        std::cout << "Copy constructor" << std::endl;
        data = new char[size];
        for (int i = 0; i < size; i++)
        {
            data[i] = p_other.data[i];
        }
    }

    MyClass(MyClass&& p_other) : size(0)
    {
        std::cout << "Move constructor" << std::endl;
        delete[] data;
        data = nullptr;
        Swap(p_other);
    }

    MyClass& operator=(MyClass&& p_other)
    {
        std::cout << "Move assignment" << std::endl;
        if (&p_other != this)
        {
            size = 0;
            delete[] data;
            data = nullptr;
            Swap(p_other);
        }
        return *this;
    }

    void Swap(MyClass& p_other)
    {
        std::swap(data, p_other.data);
        std::swap(size, p_other.size);
    }
};

int main()
{
    MyClass InstanceA(5);
    MyClass InstanceB(10);

    InstanceA = std::move(InstanceB);

    return 0;
}

The output should be:

Default constructor
Default constructor
Move assignment

Let’s dive into this code:

  • We instantiate 2 object of the same class MyClass
    • InstanceA gets a buffer with a size of 5 bytes
    • InstanceB gets a buffer with a size of 10 bytes

  • We want to transfer data from InstanceB to InstanceA
    • std::move() is called with our InstanceB as parameter
    • It will trigger the move assignment and not constructor because our InstanceA object has already called its constructor
    • We delete the data member from the InstanceA object and reset it to nullptr
    • And we swap the data members between InstanceA and InstanceB
    • Our InstanceA gets all data from InstanceB
    • InstanceB is reusable after this move

Move only types

There is some types that we can only move data from another instance. The best example is unique_ptr. Example:

auto piAuto = std::make_unique<int>(42);
auto qiAuto = std::move(piAuto);
assert(piAuto.get() == nullptr);
assert(qiAuto.get() != nullptr);
assert(*qiAuto == 42);

Not all types benefit from move semantics. In the case of built-in types (such as bool, int, or double), arrays, the move is actually a copy operation.

Perfect Forwarding

There is some case that calling only std::move() is not the most optimized way. Here an example:

#include <vector>

struct Foo
{
    int m_i;
    bool m_b;
    float m_f;

    Foo() = default;
    Foo(int i, bool b, float f)
        : m_i(i)
        , m_b(b)
        , m_f(f)
    {}
};

int main()
{
    std::vector<Foo> v1;
    {
        Foo f1(42, false, 333.3f);      // Constructor
        v1.push_back(f1);               // Copy of foo
        v1.push_back(std::move(f1));    // Move of foo
    }

    return 0;
}

If we need that our f1 variable leave outside the scope, we have 2 possibilities :

  1. Make a copy of f1
  2. Make a move of f1

It appears that we do not want to modify f1 after its instantiation. So we could write v1.push_back(Foo(42, false, 333.3f)). If we are on an IDE (Integrated Development Environment) like Visual Studio, it will advice us that we can replace this by using the emplace_back method call.

int main()
{
    std::vector<Foo> v1;
    {
        v1.emplace_back(42, false, 333.3f);
    }

    return 0;
}

The emplace_back method exploit the Perfect Forwarding concept. Here another example to explain how to apply the same behavior than this emplace_back method:

struct Foo {}; // Same struct than the example above
struct Bar
{
    void AddFoo(Foo const& foo)
    {
        v.push_back(foo);
    }
private:
    std::vector<Foo> v;
};

int main()
{
    Bar b;
    Foo f(1, true, 2.f);
    b.AddFoo(f);

    return 0;
}

We have this case of we have to instantiate a Foo object before adding into the Bar object. That would be great to write b.AddFoo(1, true, 2.f); right?

struct Bar
{
    // Variadic Templates
    template <typename ... Args>
    void AddFoo(Args&& ... p_args) // Forwarding reference
    {
        v.emplace_back(std::forward<Args>(p_args)...);
    }
private:
    std::vector<Foo> v;
};

int main()
{
    Bar b;
    //Foo f(1, true, 2.f);
    b.AddFoo(1, true, 2.f);

    return 0;
}

Here, we’re using Variadic Templates and std::forward() function. The first let us send any parameters as rvalue reference in the AddFoo method. The second apply a forwarding of the actual reference send in parameter. If we send a lvalue reference, so std::forward() will move the value as a lvalue reference. This is the same way for rvalue reference.

Tips

  • All containers from the STD are “move-aware”
  • The rule of three/five/zero
  • Avoid std::move() for the return statement. It will be see as a pessimization. The return statement gets a special traitment. It already applies a move.
  • Implementing both the move constructor and move assignment operator involves writing similar code (the entire code of the move constructor
    was also present in the move assignment operator). This can actually be avoided by calling the move assignment operator in the move constructor:
MyClass(MyClass&& p_other) : size(0), data(nullptr)
{
        std::cout << "Move constructor" << std::endl;
        *this = std::move(p_other);
}

Sources

 

Leave a Reply

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