Move Semantics
Copying a std::vector with a million items means allocating new memory and copying a million values. Often the original is never used again, for example when a function returns a vector or you put a temporary string into a container. In that case, copying is wasted work: it would be much faster to simply hand over the memory the original already owns. That hand-over is called a move, and it is one of the biggest performance features added in C++11.
In this lesson you will learn the difference between lvalues and rvalues, what std::move really does, how to give your own class a move constructor, and the common mistakes to avoid.
Copy versus move
#include <print>
#include <utility>
#include <vector>
int main() {
std::vector<int> readings(1'000'000, 7);
std::vector<int> backup = readings; // copy: a million ints duplicated
std::vector<int> archive = std::move(readings); // move: the memory is handed over
std::println("backup: {} items", backup.size());
std::println("archive: {} items", archive.size());
}backup: 1000000 items archive: 1000000 items
Both new vectors have a million items, but the work was very different. The copy allocated new memory and copied every value. The move only transferred three internal pointers from readings to archive, which takes the same tiny time whether the vector holds ten items or ten million. After the move, readings is in a valid but unspecified state: you may assign a new value to it or destroy it, but you should not rely on what it contains.
lvalues, rvalues and rvalue references
To decide between copying and moving, C++ looks at the value category of an expression. A simplified view:
- An lvalue has a name and a lasting identity, such as a variable
name. You might use it again, so C++ copies from it. - An rvalue is a temporary, such as the result of
name + " Singh"or a value returned from a function. Nobody can use it afterwards, so it is safe to move from it.
An rvalue reference, written T&&, binds only to rvalues. By overloading on const T& and T&&, a function can copy from lvalues and move from rvalues. The standard library does exactly this, for example in std::vector::push_back.
#include <print>
#include <string>
#include <utility>
void store(const std::string& s) { std::println("copying \"{}\"", s); }
void store(std::string&& s) { std::println("moving \"{}\"", s); }
int main() {
std::string name = "Kabir";
store(name); // lvalue
store(name + " Singh"); // temporary: rvalue
store(std::move(name)); // lvalue turned into an rvalue
}copying "Kabir" moving "Kabir Singh" moving "Kabir"
std::move (header <utility>) does not move anything by itself. It is only a cast that says "treat this lvalue as an rvalue, I promise not to rely on its value afterwards". The actual moving happens in the function or constructor that receives it.
Writing a move constructor
When a class is copied from an lvalue, its copy constructor runs; when it is created from an rvalue, its move constructor runs. This class prints a message in each, so you can see which one is used.
#include <print>
#include <string>
#include <utility>
class Document {
public:
explicit Document(std::string title) : title_(std::move(title)) {}
Document(const Document& other) : title_(other.title_) {
std::println("copy {}", title_);
}
Document(Document&& other) noexcept : title_(std::move(other.title_)) {
std::println("move {}", title_);
}
Document& operator=(const Document&) = default;
Document& operator=(Document&&) noexcept = default;
private:
std::string title_;
};
int main() {
Document a("Fees");
Document b = a; // a is an lvalue: copy
Document c = std::move(a); // cast to rvalue: move
Document d = Document("Notes"); // C++17: built directly in d
}copy Fees move Fees
The move constructor takes Document&& and moves each member, here by calling std::move on the string, which steals its characters. For d, nothing is printed: since C++17, initialising an object from a temporary of the same type is guaranteed to build it directly in place, with no copy and no move. This is called copy elision. Something similar happens when a function returns a local variable, as in Document make() { Document local("Report"); return local; }: the compiler is allowed to build local directly in the caller's object (named return value optimisation), and if it does not, the return statement moves the local, never copies it.
Mark move operations noexcept. Containers such as std::vector move their elements when they grow only if the move constructor promises not to throw; otherwise they copy, to keep their strong error-safety guarantee.
Moves in everyday code
You rarely write move constructors yourself: if your class holds standard types, the compiler generates correct move operations (the rule of zero). What you do write every day is code that takes advantage of moves:
#include <print>
#include <string>
#include <utility>
#include <vector>
class Contact {
public:
Contact(std::string name, std::string phone) // take by value...
: name_(std::move(name)), phone_(std::move(phone)) {} // ...then move in
const std::string& name() const { return name_; }
private:
std::string name_;
std::string phone_;
};
int main() {
std::vector<Contact> phonebook;
std::string who = "Ananya Rao";
phonebook.push_back(Contact(who, "98765 43210")); // who is copied, still usable
phonebook.emplace_back("Rohan Das", "91234 56789"); // built in place
std::string temp = "Isha Khan";
phonebook.emplace_back(std::move(temp), "99887 76655"); // temp is not needed again
for (const auto& c : phonebook) {
std::println("{}", c.name());
}
std::println("who is still {}", who);
}Ananya Rao Rohan Das Isha Khan who is still Ananya Rao
The constructor takes its strings by value and moves them into the members. A caller with an lvalue pays one copy; a caller with a temporary pays only moves. emplace_back passes its arguments to the constructor and builds the element directly inside the vector, avoiding even the temporary Contact.
Common mistakes
- Using an object after moving from it. After
std::move(name), do not readnameuntil you assign it a new value. - Writing
return std::move(local);. It blocks copy elision and can make code slower. Writereturn local;. - Moving from
constobjects.std::moveon aconstvalue produces aconstrvalue, which cannot bind toT&&, so it silently copies. - Forgetting
noexcepton move constructors.std::vectorwill then copy elements when it grows. - Adding
std::moveeverywhere. Move only when you are finished with an object; for small types such asinta move is just a copy.
Key points
- A move transfers the resources of an object that is no longer needed instead of copying them.
- lvalues have names and are copied from; rvalues are temporaries and can be moved from.
T&&binds to rvalues. std::moveis only a cast to an rvalue; the receiving constructor or function does the move.- Moved-from objects are valid but unspecified: assign or destroy them only.
- Mark move constructors
noexcept, neverreturn std::move(local);, and rely on copy elision for returned values. - Take "sink" parameters by value and
std::movethem into members; useemplace_backto build in place.
Exercise
Write a function std::vector<std::string> load_names() that creates a local vector with three names from Jaipur and returns it. In main, store the result, then move the whole vector into a second vector backup with std::move. Print the size of backup, then assign a fresh list of two names to the original vector and print its size to show that a moved-from vector can be reused.
Show solution
#include <print>
#include <string>
#include <utility>
#include <vector>
std::vector<std::string> load_names() {
std::vector<std::string> names = {"Aarav", "Priya", "Meera"};
return names; // no std::move needed
}
int main() {
std::vector<std::string> names = load_names();
std::vector<std::string> backup = std::move(names);
std::println("backup holds {} names", backup.size());
names = {"Farhan", "Isha"}; // reuse the moved-from vector
std::println("names holds {} names again", names.size());
}backup holds 3 names names holds 2 names again
After the move, names is only assigned a new value before being used again, which is always safe. The function returns its local vector without std::move, so the compiler can elide the move entirely.
Quick Quiz
1. What does std::move(x) do by itself?
std::move is just a cast; the move constructor or move assignment does the real work.
2. Which overload does store(name + "!") call when both store(const std::string&) and store(std::string&&) exist?
name + "!" is a temporary (an rvalue), so it binds to the rvalue reference overload.
3. What can you safely do with an object after moving from it?
Moved-from objects are valid but unspecified, so assigning or destroying them is safe.
4. Why should move constructors be noexcept?
Vectors move elements during reallocation only if the move cannot throw.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
