C++ Move Semantics

日月星辰 发布在Programming

rvalues & rvalue references

// lvalue reference parameter
void handleMsg(std::string& msg)
{
    cout << "handleMsg with lvalue reference: " << msg << endl;
}
// rvalue reference parameter
void handleMsg(std::string&& msg)
{
    cout << "handleMsg with rvalue reference: " << msg << endl;
}
// Example
std::string a = "Hello ";
std::string b = "World";
handleMsg(a); // Calls handleMsg(string& value)
handleMsg(a + b) // Calls handleMsg(string&& value)

// If remove the handleMsg() accepting lvalue reference, 
// calling handleMsg(b) will result in a compilation error,
// rvalue reference parameter will never bound to a lvalue.
// You can force to call rvalue reference version by using 
// std::move(), which casts a lvalue into a rvalue

handleMsg(std::move(b)); // Calls handleMsg(string&& value)

A named variable is lvalue, so inside the handleMsg(string&& value) function, the value is a lvalue because it has a name,
If you want to forward the rvalue reference parameter to another function as an rvalue, you need to use std::move() to cast the lvalue to rvalue.

// rvalue reference parameter
void handleMsg(std::string&& msg)
{
    cout << "handleMsg with rvalue reference: " << msg << endl;
    helper(std::move(msg)); // cast to rvalue
}
// rvalue reference parameter
void helper(std::string&& msg)
{
    ...
}

Implementing Move Semantics

To add move semantics to a class, you need to implement a move constructor and a move assignment operator, which should be marked with the noexcept qualifier to tell the compiler that they don't throw any exceptions.

class Simple 
{
    public:
        Simple(Simple&& src) noexcept; // move constructor
        Simple& operator=(Simple&& rhs) noexcept; // move assign
};

Move constructors and move assignment operators can be explicitly deleted or defaulted.

The compiler automatically generates a default move constructor for a class if and only if the class has no user-declared copy constructor, copy assignment operator, move assignment operator, or destructor.
A default move assignment operator is generated for a class if and only if the class has no user-declared copy constructor, move constructor, copy assignment operator, or destructor.

Rule of Five1
If you have dynamically allocated memory in your class, then you typically should implement a destructor, copy constructor, move constructor, copy assignment operator, and move assignment operator.

Moving Object Data Members

class Simple 
{
    private:
        Simple() = default;
    public:
        // move constructor
        Simple(Simple&& src) noexcept 
             : Simple()
        {
             swap(*this, src);
        }
        // move assign
        Simple& operator=(Simple&& rhs) noexcept
        {
               Simple temp(std::move(rhs));
               swap(*this, temp);
               return *this;
        }
};

You only have to update your swap() implementation to include the new data members.

Two different implementation of swap:

void swapCopy(T& a, T& b)
{
    T temp(a);
    a = b;
    b = temp;
}

void swapMove(T& a, T& b)
{
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}

Rule of zero2
you should design your classes in such a way that they do not require any of those five special member functions1.
In modern C++, adopt the rule of zero!

Footnotes

  1. Rule of Five 2

  2. Rule of Zero