Move semantics is why returning big objects by value is cheap in modern C++, and you need it to write efficient resource-owning types.
Copying a std::vector duplicates all its elements. But if the source is about to die anyway, such as a temporary or a local being returned, we could just steal its heap buffer. Move semantics does exactly that.
Expressions have value categories. An lvalue has a name and an identity (x, v[0]); an rvalue is a temporary (f(), a + b, Buffer(8)). An rvalue reference, T&&, binds only to rvalues, so overloads can tell "copy from something that stays alive" from "take from something that's going away":
T(const T&)is the copy constructor;T(T&&)is the move constructor, which takes the resource and leaves the source empty but valid.- The same pair exists for assignment operators.
std::move(x) doesn't move anything. It's a cast to T&& that says "I'm done with x, you may steal from it". After a move, the object is in a valid but unspecified state: you may destroy it or assign a new value, but don't rely on its contents. (Standard containers do document that a moved-from vector is empty.)
Rules that matter:
- Mark move operations
noexcept, orstd::vectorwill copy instead of move when it grows. - Return locals by plain
return x;. The compiler elides the copy (NRVO) or moves implicitly. Writingreturn std::move(x);can block the elision. - Since C++17, returning or initializing from a prvalue (
return Buffer(1024);) is guaranteed to construct in place, with no copy or move at all. - Moving a
constobject silently copies, becauseconst T&&can't bind to the move constructor.
For functions that store their argument, a common idiom is to take it by value and move it in: void set(std::string s) { name_ = std::move(s); }.
Example
#include <iostream>
#include <utility>
#include <vector>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(n) {}
Buffer(const Buffer& o) : data_(o.data_) { std::cout << "copy\n"; }
Buffer(Buffer&& o) noexcept : data_(std::move(o.data_)) { std::cout << "move\n"; }
Buffer& operator=(const Buffer&) = default;
Buffer& operator=(Buffer&&) noexcept = default;
std::size_t size() const { return data_.size(); }
private:
std::vector<char> data_;
};
Buffer make() { Buffer b(1024); return b; } // NRVO or implicit move, never a copy
int main() {
Buffer a(100);
Buffer b = a; // copy: a is an lvalue
Buffer c = std::move(a); // move: std::move casts a to Buffer&&
Buffer d = make(); // typically prints nothing: elided
std::vector<Buffer> v;
v.push_back(Buffer(8)); // move: the temporary is an rvalue
std::cout << b.size() << ' ' << c.size() << ' ' << d.size() << '\n'; // 100 100 1024
}Remember
std::moveis only a cast; the move constructor does the stealing- Moved-from objects are valid but unspecified: assign or destroy them only
- Return locals by name; don't
return std::move(local); - Move operations should be
noexcept
Watch out for
- Calling
std::moveon aconstobject and getting a silent copy.