Common misconception with C++ move semantics

Move semantics have to be one of the most prominent new features in C++11. It is also something that can be a source of misunderstandings if the underlying mechanics are not fully understood. The fact is that std::move does not actually move anything. Yet, its name would suggest otherwise. So, let’s clear up this misconception.

First, lets look at an example that shows move semantics in action:

std::vector<int> a = {1, 2, 3, 4, 5};
auto b = std::move(a);

std::cout << "a: " << a.size() << std::endl;
std::cout << "b: " << b.size() << std::endl;

Intuitively, what will be the sizes of a and b? Well, this example works exactly as you would expect it to. The contents of vector a are moved to b, so size of a is zero and size of b is five (to be exact, after move, a is guaranteed to be in a valid but unspecified state. For most implementations the size is zero).

Here’s another example. A class that can be constructed from a vector of integers. Instance d is constructed from std::move(a)

class Data {
public:
    Data(const std::vector<int>& data): m_data(data) {}

    size_t size() const { return m_data.size(); }

private:
    std::vector<int> m_data;
};

int main() {
    std::vector<int> a = {1, 2, 3, 4, 5};

    auto d = Data(std::move(a));
    std::cout << "a: " << a.size() << std::endl;
    std::cout << "d: " << d.size() << std::endl;

    return 0;
}

In this case the size of both a and d is five, so the vector was copied! Yet, in both examples above std::move(a) was the constructor argument. First for vector and then for Data, but the behaviour was different.

So, in one situation applying std::move did move the value whereas in other situation it didn’t. That brings us back to the initial point: std::move does not actually move anything. Cppreference.com has this to say about move:

std::move is used to indicate that an object t may be “moved from”, i.e. allowing the efficient transfer of resources from t to another object.

To understand how the move is indicated, it is necessary to understand what lvalues and rvalues are. As a rule of thumb, lvalue refers to a memory location and it is possible to take its address (e.g. parameter name, function, data member) whereas rvalues are expressions that result in a temporary object (here’s a more comprehensive definition).

// a is lvalue, {1, 2, 3,} is rvalue
std::vector<int> a = {1, 2, 3,};

Moving is especially useful when working with temporary objects, rvalues. Moving can prevent unnecessary copying when temporary objects are passed as parameter or returned. When object is a rvalue, it may be moved from transferring its contents to another object. So we only need a mechanism to distinguish between rvalues and lvalues and we’re good to go. This can be done with rvalue references that are denoted with T&&.

To make move work with the Data class shown earlier, another constructor that takes rvalue reference is added.

// takes lvalue reference as parameter
Data(const std::vector<int>& data): m_data(data) {}

// takes rvalue reference as parameter
Data(std::vector<int>&& data): m_data(std::move(data)) {}

When lvalue is passed, it is copied and when rvalue is passed, it is moved from. The final component is std::move. What does it actually do? Again, from cppreference.com:

In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

That’s all there is to it. It casts its argument to rvalue reference and neither moves anything nor produces any runtime code. It is only a type cast. Given this information we could have written the example earlier also with static_cast as follows:

// static_cast instead of std::move
auto d = Data(static_cast<std::vector<int>&&>(a));

When a is cast to rvalue reference, it will use the rvalue constructor from Data which in turn moves from the supplied parameter. Moving from vector to vector, of course, works because the standard library implements move constructors for containers. The mechanism is still the same.

Note that it is necessary to write m_data(std::move(data)) in the constructor implementation because “Even if the variable’s type is rvalue reference, the expression consisting of its name is a lvalue expression”. Also, the first version of Data compiled because rvalues can also bind to const lvalue reference (e.g. const std::vector<int>&).

That’s it. Now you know the basics of what std::move does do and what it doesn’t. It is important to remember that the move support is implemented by the target class, and std::move only casts it parameter to rvalue reference indicating that the value is eligible for move. The same constructor is also called when rvalue that is not the result of std::move is passed, for instance auto c = Data({1,2,3,4});.

6 thoughts on “Common misconception with C++ move semantics

  1. That case works similarly as the first example where “a” was moved directly to vector “b” since the implementation of move constructor in Data invokes the move constructor of vector. So the size of “a” is 0 (again to be exact, the 0 size is not guaranteed since the standard says that the moved from object will be in unspecified but valid state. For most implementations it will be zero)

    Like

  2. My understanding is that If I have 2 vectors a, and b. If I move b to a, the underlying memory region’s pointer is transferred, and b relinquishes any control over it. b’s internal pointer would no be null.

    Like

  3. If you want a guarantee the moved from object is empty, you can use std::exchange(vector, {}).
    Returns an rvalue just like std::move, but assigns {} (empty initializer list) to vector.

    Liked by 1 person

  4. if you defined a constructor with this signature:
    Data(const std::vector data): m_data(data) {}
    the example would’ve worked without further modifications.
    that it’s because the rvalue reference is being caught directly by std::vector move constructor.

    Like

Leave a comment