C++ course menu
C++ Move Semantics: rvalues, std::move and the Rule of Five
Understand C++ move semantics: lvalues and rvalues, rvalue references (&&), what std::move really does, move constructors and move assignment, noexcept moves, the Rule of Three, Rule of Five and Rule of Zero, and copy elision.
On this page
Copying a large object, such as a vector with a million elements, means allocating new memory and copying every element. But often the original is about to be thrown away anyway: a temporary result, or a local variable being returned. Move semantics (C++11) lets C++ steal the resources of such an object instead of copying them, which is often dramatically faster.
lvalues and rvalues#
Roughly:
- An lvalue has a name and an address; it persists beyond the expression (a variable such as
name). - An rvalue is a temporary that is about to disappear (
std::string("hi"),a + b, a function's return value).
It is safe to steal from an rvalue, because nobody can use it afterwards. An rvalue reference, written T&&, binds only to rvalues, and that is how a class detects "this argument may be moved from".
Watching copies and moves#
This class prints which special member function runs:
#include <iostream>
#include <string>
#include <utility>
class Buffer {
public:
explicit Buffer(std::string name) : name_(std::move(name)) {}
Buffer(const Buffer& other) : name_(other.name_) {
std::cout << "copy " << name_ << '\n';
}
Buffer(Buffer&& other) noexcept : name_(std::move(other.name_)) {
std::cout << "move " << name_ << '\n';
}
private:
std::string name_;
};
int main() {
Buffer a("A");
Buffer b = a; // a is an lvalue: copy
Buffer c = std::move(a); // std::move makes it an rvalue: move
Buffer d = Buffer("D"); // a temporary: no copy or move at all (elision)
}copy A
move AThe last line prints nothing: since C++17, initializing from a temporary of the same type constructs the object directly in place (guaranteed copy elision).
What std::move does (and doesn't)#
std::move does not move anything. It is just a cast that says "treat this as an rvalue; I am done with it". The actual moving happens in the move constructor or move assignment operator that the cast makes the compiler choose.
After being moved from, an object is in a valid but unspecified state. You may assign it a new value or destroy it, but do not rely on its contents:
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<std::string> names;
std::string name = "Asha Rao";
names.push_back(name); // copies: name is still needed
names.push_back(std::move(name)); // moves: name's text is taken
std::cout << names[0] << " | " << names[1] << '\n';
name = "Ravi"; // fine: give it a new value
std::cout << name << '\n';
}Asha Rao | Asha Rao
RaviWriting move operations#
For a class that owns a raw resource, the move constructor takes the other object's pointer and leaves the source empty, so its destructor does not free the stolen memory:
#include <algorithm>
#include <iostream>
#include <utility>
class IntArray {
public:
explicit IntArray(std::size_t n) : size_(n), data_(new int[n]{}) {}
~IntArray() { delete[] data_; }
// copy: duplicate the resource
IntArray(const IntArray& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
IntArray& operator=(const IntArray& other) {
if (this != &other) {
IntArray temp(other); // copy first, then swap: safe if new throws
swap(temp);
}
return *this;
}
// move: steal the resource
IntArray(IntArray&& other) noexcept
: size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {}
IntArray& operator=(IntArray&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = std::exchange(other.size_, 0);
data_ = std::exchange(other.data_, nullptr);
}
return *this;
}
void swap(IntArray& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}
std::size_t size() const { return size_; }
int& operator[](std::size_t i) { return data_[i]; }
private:
std::size_t size_;
int* data_;
};
int main() {
IntArray a(3);
a[0] = 42;
IntArray b = a; // copy
IntArray c = std::move(a); // move: a is now empty
std::cout << b[0] << ' ' << c[0] << ' ' << a.size() << '\n';
}42 42 0std::exchange(x, new_value) sets x to new_value and returns its old value, which is exactly what stealing needs.
Mark move operations noexcept. Containers such as std::vector only move elements during reallocation if the move constructor cannot throw; otherwise they copy, to keep their strong exception guarantee.
The Rule of Three, Five and Zero#
- Rule of Three: if a class needs a custom destructor, copy constructor or copy assignment, it almost certainly needs all three (because it manages a resource).
- Rule of Five: in modern C++, add the move constructor and move assignment too.
- Rule of Zero: best of all, design classes so they need none of the five. Hold resources in members that already manage themselves (
std::vector,std::string,std::unique_ptr), and the compiler-generated versions do the right thing, including moves.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
class Playlist { // Rule of Zero: no special members written
public:
explicit Playlist(std::string name) : name_(std::move(name)) {}
void add(std::string song) { songs_.push_back(std::move(song)); }
std::size_t size() const { return songs_.size(); }
private:
std::string name_;
std::vector<std::string> songs_;
};
int main() {
Playlist p("Focus");
p.add("Track 1");
p.add("Track 2");
Playlist q = std::move(p); // generated move constructor moves both members
std::cout << q.size() << '\n';
}2Writing the five special members by hand is mainly for low-level resource-owning classes, like the IntArray above. Everyday classes should follow the Rule of Zero.
Passing and returning in modern C++#
- Return local objects by value:
return result;. The compiler elides the copy or moves automatically. Writingreturn std::move(result);can actually prevent elision. - Sink parameters: when a function stores its argument (a constructor saving a name), take it by value and
std::moveit into place, as inPlaylist(std::string name) : name_(std::move(name)). Callers passing a temporary then pay for one move instead of a copy.
Common mistakes#
Practice#
Practice exercise
Create three local strings, "alpha", "beta" and "gamma", and push_back them into a std::vector<std::string>: the first by copy and the other two with std::move. Print the vector, then count how many of the three local strings are now empty.
Show solution
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string a = "alpha";
std::string b = "beta";
std::string c = "gamma";
std::vector<std::string> v;
v.push_back(a);
v.push_back(std::move(b));
v.push_back(std::move(c));
for (const auto& s : v) std::cout << s << ' ';
std::cout << '\n';
int empty = 0;
for (const std::string* s : {&a, &b, &c}) {
if (s->empty()) ++empty;
}
std::cout << empty << " of the locals are empty\n";
}alpha beta gamma
2 of the locals are emptyMoved-from standard strings are left empty in practice by GCC, Clang and MSVC, but the standard only promises a "valid but unspecified" state, so real code should not depend on it.
Key takeaway#
Finished this lesson?