Moving
Transferring ownership instead of duplicating, and what std::move really does.
By the end of this chapter you can
- Explain what std::move does and does not do
- Write a move constructor that leaves the source valid
- Predict when the compiler moves instead of copies
The last chapter ended with a measurement: copying a two-million-element vector took milliseconds, and moving it took no measurable time at all. This chapter is about why, and how to give your own types the same property.
The idea is simple. When the source object is about to be destroyed anyway, duplicating its contents is waste. Instead of copying the data, take it — and leave the source in a state that is safe to destroy.
Copy versus move, side by side
#include <iostream>
#include <string>
#include <utility>
struct Tracked {
std::string data;
Tracked(std::string d) : data(std::move(d)) {}
Tracked(const Tracked& other) : data(other.data) {
std::cout << " copy: duplicated " << data.size() << " bytes\n";
}
Tracked(Tracked&& other) noexcept : data(std::move(other.data)) {
std::cout << " move: took the buffer, source now has "
<< other.data.size() << " bytes\n";
}
};
int main() {
Tracked original{std::string(1000, 'x')};
std::cout << "copying:\n";
Tracked copied = original;
std::cout << "moving:\n";
Tracked moved = std::move(original);
std::cout << "original now holds " << original.data.size() << " bytes\n";
}The copy allocated a thousand bytes and duplicated them. The move copied a pointer, a size, and a capacity — three machine words — and left the source empty.
Note the source is empty, not broken. That distinction is the whole contract of a move.
What std::move actually does
This is the most misunderstood name in the standard library. std::move does
not move anything. It performs a cast.
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string source = "still here";
// std::move only produces an rvalue reference. Nothing has happened yet.
std::string&& reference = std::move(source);
std::cout << "after std::move alone: \"" << source << "\"\n";
std::cout << "(nothing was moved — no constructor ran)\n";
// The move happens HERE, because this initialisation selects the
// move constructor.
std::string destination = std::move(source);
std::cout << "after constructing from it: \"" << source << "\"\n";
std::cout << "destination: \"" << destination << "\"\n";
}std::move(x) casts x to an rvalue reference. That changes which overload
gets selected — a constructor or assignment operator taking T&& becomes
viable, and it is preferred over the one taking const T&. The actual transfer
is done by that constructor.
Read std::move(x) as “I am done with x; you may take from it if you like.”
It is a permission, not an action. If nothing takes it, nothing happens.
lvalues and rvalues
Two words for a distinction the language has always had. Loosely:
- An lvalue is an expression naming an object with an identity you can take the address of. Variables are lvalues.
- An rvalue is a temporary — the result of an expression that is about to vanish. Function return values and literals are rvalues.
The compiler uses this to decide, on its own, when moving is safe:
#include <iostream>
#include <string>
#include <vector>
struct Noisy {
std::vector<int> data;
Noisy(std::size_t n) : data(n) {}
Noisy(const Noisy& o) : data(o.data) { std::cout << " copy\n"; }
Noisy(Noisy&& o) noexcept : data(std::move(o.data)) { std::cout << " move\n"; }
};
Noisy make() { return Noisy{100}; }
int main() {
std::cout << "from a named variable:\n";
Noisy a{100};
Noisy b = a; // a is an lvalue: copy
std::cout << "from a temporary:\n";
Noisy c = make(); // the result is an rvalue: move (or elided)
std::cout << "from a named variable, with permission:\n";
Noisy d = std::move(a); // cast to rvalue: move
std::cout << "sizes: " << b.data.size() << ' ' << c.data.size()
<< ' ' << d.data.size() << '\n';
}You do not need std::move for temporaries — the compiler already knows they
are expendable. You need it when you want to give up a named object, because
the compiler must assume a name might be used again.
Writing move operations
A move constructor takes T&&, steals the resources, and leaves the source in a
state that is valid to destroy and valid to assign to.
#include <cstddef>
#include <iostream>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), size_(n) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
for (std::size_t i = 0; i < size_; ++i) data_[i] = other.data_[i];
std::cout << " copied " << size_ << " ints\n";
}
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) { // take
other.data_ = nullptr; // and leave it empty
other.size_ = 0;
std::cout << " moved (no allocation)\n";
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // release ours
data_ = other.data_; // take theirs
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
int* fresh = new int[other.size_];
for (std::size_t i = 0; i < other.size_; ++i) fresh[i] = other.data_[i];
delete[] data_;
data_ = fresh;
size_ = other.size_;
}
return *this;
}
std::size_t size() const { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a{1000};
Buffer b = a; // copy
Buffer c = std::move(a); // move
std::cout << "a.size() = " << a.size() << " (moved from)\n";
std::cout << "c.size() = " << c.size() << '\n';
Buffer d{5};
d = std::move(c); // move assignment
std::cout << "d.size() = " << d.size() << ", c.size() = " << c.size() << '\n';
}Three details in that code carry weight.
Null the source pointer. Not because it is tidy, but because the source’s
destructor will still run, and delete[] on the stolen pointer would free the
buffer the destination now owns. delete[] nullptr is defined to do nothing,
which is why nulling is sufficient.
Set the source’s size to zero. The moved-from object must be consistent,
not merely destructible. Leaving size_ at 1000 with a null data_ makes any
later size() call a lie and any indexing a crash.
Mark them noexcept. This is not decoration — see below.
What a moved-from object may be
The standard requires that a moved-from standard-library object be in a valid but unspecified state. Valid means you may safely destroy it, assign to it, and call any operation with no precondition. Unspecified means you may not assume what it contains.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> source{1, 2, 3};
std::vector<int> taken = std::move(source);
// Legal: assigning gives it a definite value again.
source = {7, 8};
std::cout << "reassigned: " << source.size() << " elements\n";
std::string text = "hello";
std::string stolen = std::move(text);
// Legal but unwise: clear() has no precondition, so this is defined —
// but relying on what text held first would not be.
text.clear();
text = "reused";
std::cout << "text is usable again: " << text << '\n';
}In practice, implementations leave a moved-from vector or string empty, and
it is tempting to rely on that. Do not: it is not guaranteed, and for your own
types you should promise only what you actually enforce. The safe rule is
assign to a moved-from object before reading it.
noexcept is not optional
std::vector grows by allocating a bigger block and transferring the elements.
If it moves them and a move throws halfway through, the old block is already
half-destroyed and there is no way back. So vector only moves your elements if
their move constructor promises not to throw. Otherwise it copies.
#include <chrono>
#include <iostream>
#include <string>
#include <vector>
struct WithNoexcept {
std::string data;
WithNoexcept(std::size_t n) : data(n, 'x') {}
WithNoexcept(const WithNoexcept&) = default;
WithNoexcept(WithNoexcept&& o) noexcept : data(std::move(o.data)) {}
WithNoexcept& operator=(WithNoexcept&&) noexcept = default;
};
struct WithoutNoexcept {
std::string data;
WithoutNoexcept(std::size_t n) : data(n, 'x') {}
WithoutNoexcept(const WithoutNoexcept&) = default;
WithoutNoexcept(WithoutNoexcept&& o) : data(std::move(o.data)) {} // no noexcept
WithoutNoexcept& operator=(WithoutNoexcept&&) = default;
};
template <class T>
long long time_growth() {
using clock = std::chrono::steady_clock;
auto start = clock::now();
std::vector<T> v;
for (int i = 0; i < 20000; ++i) v.emplace_back(200); // forces reallocations
auto finish = clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start).count();
}
int main() {
std::cout << "move is noexcept: " << time_growth<WithNoexcept>() << " ms\n";
std::cout << "move may throw: " << time_growth<WithoutNoexcept>() << " ms\n";
}Same code, same data, and the only difference is one keyword. Every move
operation you write should be noexcept, and a correct one always can be — it
transfers pointers and never allocates.
Where moves happen without you asking
#include <iostream>
#include <string>
#include <vector>
std::vector<std::string> collect() {
std::vector<std::string> result;
result.push_back("alpha");
result.push_back(std::string(1000, 'b')); // temporary: moved in
return result; // no copy: elided, or moved
}
int main() {
std::vector<std::string> values = collect();
std::cout << values.size() << " items, second is "
<< values[1].size() << " bytes\n";
std::vector<std::string> destination;
destination.push_back(std::move(values[0])); // explicit: we are done with it
std::cout << "moved-from element is now " << values[0].size() << " bytes\n";
}Returning a local by value does not copy. Since C++17 the compiler is required
to elide the copy in many cases, and where it cannot elide, it moves. return result; is the right way to return a large object, and writing
return std::move(result); is actively worse — it prevents the elision that
would otherwise remove the operation entirely. Chapter 7.5 covers exactly when.