Move constructors and move assignment

A copy duplicates a resource; a move transfers it. For a buffer of a million ints the difference is one allocation and a million copies against three pointer assignments, and the standard library moves whenever it is allowed to.

The tool is the rvalue reference, T&&, which binds to a temporary or to a value the caller explicitly gives up with std::move. This lesson writes the two move operations that lesson 031's buffer was missing, and shows where the standard library uses them.

An rvalue reference parameter

An overload taking const T& accepts everything and copies nothing, and an overload taking T&& accepts only temporaries and values passed through std::move. Given both, the compiler prefers the second for an rvalue.

The program calls the same function three times: with a named string, with std::move(name) and with a literal. The first picks the const reference, the other two pick the rvalue reference, and the output names the chosen overload.

std::move does not move anything by itself: it casts its argument to an rvalue so the overload for T&& can be selected. The move happens inside the function that takes the reference.

#include <iostream> #include <string> #include <utility> void take(const std::string& value) { std::cout << "const ref: " << value << '\n'; } void take(std::string&& value) { std::cout << "rvalue ref: " << value << '\n'; } int main() { std::string name = "Ada"; take(name); take(std::move(name)); take("literal"); }

g++ -std=c++23 -Wall -Wextra rvalue.cpp -o rvalue
./rvalue

const ref: Ada
rvalue ref: Ada
rvalue ref: literal

A move constructor

The move constructor takes the other object's pointer, gives it to the new object and leaves the source empty. No allocation and no element copy happen, and the source is still a valid object that can be destroyed.

The buffer below has a copy constructor that prints copy and a move constructor that prints move. Moving a buffer prints one move line, keeps the values and leaves the moved-from size at 0.

The move constructor is marked noexcept, and that matters: a vector that has to reallocate uses the move only when it is noexcept, because a throwing move would leave the vector unable to restore its old elements. Lesson 080 explains the rule.

#include <iostream> #include <utility> class buffer { public: explicit buffer(int size) : size_(size), data_(new int[size]{}) {} buffer(const buffer& other) : size_(other.size_), data_(new int[other.size_]) { for (int i = 0; i < size_; ++i) { data_[i] = other.data_[i]; } std::cout << "copy\n"; } buffer(buffer&& other) noexcept : size_(other.size_), data_(other.data_) { other.size_ = 0; other.data_ = nullptr; std::cout << "move\n"; } ~buffer() { delete[] data_; } int& at(int index) { return data_[index]; } int size() const { return size_; } private: int size_; int* data_; }; int main() { buffer a{3}; a.at(0) = 7; buffer b = std::move(a); std::cout << b.at(0) << ' ' << b.size() << ' ' << a.size() << '\n'; }

g++ -std=c++23 -Wall -Wextra move_ctor.cpp -o move_ctor
./move_ctor

move
7 3 0

Move assignment

Move assignment releases the target's old resource, takes the source's pointer and empties the source. Like copy assignment it must survive a = std::move(a), so it checks for self-assignment before it frees anything.

The program moves one buffer into another that already owns an array. The old array is freed, the pointer is transferred, and the printed value comes from the buffer that was moved from.

A moved-from object is valid but unspecified: the lesson keeps it empty, which is the usual choice. Code may destroy it, assign to it or call a function that does not read its value, and nothing else.

#include <iostream> #include <utility> class buffer { public: explicit buffer(int size) : size_(size), data_(new int[size]{}) {} buffer(const buffer&) = delete; buffer& operator=(const buffer&) = delete; buffer(buffer&& other) noexcept : size_(other.size_), data_(other.data_) { other.size_ = 0; other.data_ = nullptr; } buffer& operator=(buffer&& other) noexcept { if (this == &other) { return *this; } delete[] data_; size_ = other.size_; data_ = other.data_; other.size_ = 0; other.data_ = nullptr; return *this; } ~buffer() { delete[] data_; } int& at(int index) { return data_[index]; } int size() const { return size_; } private: int size_; int* data_; }; int main() { buffer target{2}; target.at(0) = 1; buffer source{2}; source.at(0) = 9; target = std::move(source); std::cout << target.at(0) << ' ' << target.size() << ' ' << source.size() << '\n'; }

g++ -std=c++23 -Wall -Wextra move_assign.cpp -o move_assign
./move_assign

9 2 0

Where the standard library moves

A vector that outgrows its buffer has to move every element into the new one. The item class below counts copies and moves, and the program reserves one slot and then pushes two elements, which forces exactly one reallocation.

The output is three moves and no copies: one for the temporary that push_back takes, one for the element already in the vector and one for the new element. Because the move constructor is noexcept, the vector is allowed to move.

Remove the noexcept and the same program copies instead, because the vector falls back to the copy constructor when a throwing move could lose data. That single keyword is the difference between a fast reallocation and a slow one.

#include <iostream> #include <utility> #include <vector> int copies = 0; int moves = 0; class item { public: item() = default; item(const item&) { ++copies; } item(item&&) noexcept { ++moves; } item& operator=(const item&) = default; item& operator=(item&&) = default; }; int main() { std::vector<item> items; items.reserve(1); items.push_back(item{}); items.push_back(item{}); std::cout << "copies: " << copies << " moves: " << moves << '\n'; }

g++ -std=c++23 -Wall -Wextra vector.cpp -o vector_move
./vector_move

copies: 0 moves: 3

An rvalue reference does not bind to an lvalue

A function taking int&& accepts a temporary or an explicit std::move, and nothing else. Passing a named variable is an error, because the caller has not given up the value and the function would be free to steal it.

The message says a reference of type int&& cannot bind to an lvalue of type int. The fix is std::move(x) at the call, which is a statement in the source that the value is no longer needed.

The same rule protects every implicitly generated move in a program: a named variable is an lvalue, and it takes an explicit std::move to pass it to a move constructor.

void take(int&& value) { (void)value; } int main() { int x = 1; take(x); }

g++ -std=c++23 -Wall -Wextra lvalue.cpp -o lvalue

lvalue.cpp: In function 'int main()':
lvalue.cpp:9:10: error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
9 | take(x);
| ^
lvalue.cpp:1:17: note: initializing argument 1 of 'void take(int&&)'
1 | void take(int&& value)
| ~~~~~~^~~~~

Exercises

  1. Add a move constructor to the buffer in deep.cpp of lesson 031 and print both moved-from and moved-to sizes.
  2. Remove noexcept from item's move constructor and rerun the vector program. How many copies appear instead of moves?
  3. Write a function that takes std::vector&& and clears it, and call it with std::move on a named vector.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does T&& bind to?

    Answer

    An rvalue such as a temporary or a std::move result

    The rvalue reference is the parameter type that lets a function take a value the caller gives up.

  2. What does std::move(x) do?

    Answer

    It casts x to an rvalue reference

    The function is a cast; the actual transfer happens in the constructor or assignment that receives it.

  3. What is left in the source of a move constructor?

    Answer

    A valid object with no resource, such as a null pointer

    The source must still be destructible, so the constructor sets its pointer to null and its size to zero.

  4. Why should a move constructor be noexcept?

    Answer

    A vector uses move only when it cannot throw

    A throwing move could not be undone during a reallocation, so the container falls back to copying.

  5. What does this program print? #include <iostream> #include <string> #include <utility> void take(const std::string& text) { std::cout << "const ref: " << text << '\n'; } void take(std::string&& text) { std::cout << "rvalue ref: " << text << '\n'; } int main() { std::string name = "Ada"; take(name); take("Ada"); take(std::string{"literal"}); }

    Answer

    const ref: Ada / rvalue ref: Ada / rvalue ref: literal

    The named string binds to the const reference, and the two temporaries bind to the rvalue reference.

  6. What does this program print? #include <iostream> #include <utility> class buffer { public: explicit buffer(int size) : data_(new int[size]), size_(size) {} buffer(buffer&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; other.size_ = 0; std::cout << "move\n"; } ~buffer() { delete[] data_; } int* data_; int size_; }; int main() { buffer first{3}; first.data_[0] = 7; buffer second = std::move(first); std::cout << second.size_ << ' ' << second.data_[0] << ' ' << first.size_; }

    Answer

    move / 3 7 0

    The move prints, transfers the buffer and leaves the source with zero elements.

  7. What does this program print? #include <iostream> #include <utility> class buffer { public: explicit buffer(int size) : size_(size) {} buffer& operator=(buffer&& other) noexcept { size_ = other.size_; other.size_ = 0; return *this; } ~buffer() = default; int size_; int other_ = 2; }; int main() { buffer first{9}; buffer second{2}; second = std::move(first); std::cout << second.size_ << ' ' << second.other_ << ' ' << first.size_; }

    Answer

    9 2 0

    The assignment takes the source's size and leaves the source empty; the third value is the printed constant.

  8. What does this program print? #include <iostream> #include <utility> #include <vector> int copies = 0, moves = 0; struct item { item() = default; item(const item&) { ++copies; } item(item&&) noexcept { ++moves; } }; int main() { std::vector<item> values; values.reserve(3); values.push_back(item{}); values.push_back(item{}); values.push_back(item{}); std::cout << "copies: " << copies << " moves: " << moves; }

    Answer

    copies: 0 moves: 3

    The reserve prevents reallocation, so each push_back moves its temporary and no element is copied.

  9. What does passing an lvalue to void take(int&&) produce?

    Answer

    cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'

    An rvalue reference does not bind to a named object without std::move.

  10. Why does std::move on a named local matter at a call site?

    Answer

    Without it the argument is an lvalue and a copy is chosen

    The cast is what tells the compiler that the caller gives up the object.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev