Memory, Smart Pointers and Move · Lesson 14 of 33
Move Semantics and the Rule of Five
Copying a vector of a million elements copies a million elements. But if the original is about to be thrown away anyway, why not just take its memory? That's move semantics (C++11): transferring resources instead of copying them. It's why returning a big vector from a function is cheap, and it's a standard interview topic for experienced C++ roles.
In this lesson
- lvalues, rvalues and rvalue references
- Copy vs move, and
std::move - Writing a move constructor and move assignment
- The rules of zero, three and five
Copy vs move
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<int> big(1'000'000, 7);
std::vector<int> copy = big; // copies a million ints
std::vector<int> moved = std::move(big); // steals big's buffer: no element is copied
std::cout << "copy: " << copy.size() << ", moved: " << moved.size()
<< ", big after move: " << big.size() << '\n'; // valid but unspecified – empty in practice
std::string name = "Asha Verma, Computer Science";
std::vector<std::string> list;
list.push_back(name); // copy: name still needed
list.push_back(std::move(name)); // move: we're done with name
std::cout << list[1] << " | name is now \"" << name << "\"\n";
return 0;
}
copy: 1000000, moved: 1000000, big after move: 0 Asha Verma, Computer Science | name is now ""
What std::move(big) does
- std::movedoesn't move anything – it just casts big to an rvalue reference ("you may steal from me")
- The move constructor of movedtakes big's pointer, size and capacity
- big is left emptyin a valid state: you may assign to it or destroy it, but don't rely on its value
lvalues and rvalues
- An lvalue has a name and an address:
name,v[0]. It may be used again, so it's copied. - An rvalue is a temporary about to disappear:
a + b,std::string("x"), a function's return value. It can safely be moved from. T&&is an rvalue reference: it binds to temporaries (and to things youstd::move).
Writing move operations
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <utility>
class Buffer { // owns a raw array (for teaching; prefer std::vector)
public:
explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) { std::cout << "allocate " << n << '\n'; }
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) { // copy constructor
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "copy " << size_ << '\n';
}
Buffer(Buffer&& other) noexcept // move constructor
: size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {
std::cout << "move " << size_ << '\n';
}
Buffer& operator=(Buffer other) noexcept { // copy-and-swap: handles copy AND move assignment
std::swap(size_, other.size_);
std::swap(data_, other.data_);
return *this;
}
std::size_t size() const { return size_; }
private:
std::size_t size_;
int* data_;
};
Buffer make_buffer() { return Buffer(3); } // returned without copying (copy elision)
int main() {
Buffer a(5);
Buffer b = a; // copy
Buffer c = std::move(a); // move
Buffer d = make_buffer(); // no copy, no move
b = Buffer(8); // move-assign a temporary
std::cout << a.size() << ' ' << b.size() << ' ' << c.size() << ' ' << d.size() << '\n';
return 0;
}
allocate 5 copy 5 move 5 allocate 3 allocate 8 0 8 5 3
The move constructor just takes the other object's pointer and leaves it empty – constant time, whatever the size. Marking it noexcept matters: std::vector only uses moves when growing if they can't throw.
The rules of zero, three and five
| Rule | Says |
|---|---|
| Rule of zero | Prefer classes that manage no raw resources themselves – use vector, string and smart pointers as members, and write none of the special functions. The compiler generates correct ones. |
| Rule of three (pre-C++11) | If you write a destructor, copy constructor or copy assignment, you probably need all three. |
| Rule of five | If you manage a resource by hand, write all five: destructor, copy constructor, copy assignment, move constructor, move assignment. |
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Playlist { // rule of zero: no destructor, no copy/move code needed
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(); }
const std::string& name() const { return name_; }
private:
std::string name_;
std::vector<std::string> songs_; // members clean up and copy/move themselves
};
int main() {
Playlist p("Focus");
p.add("Song A");
p.add("Song B");
Playlist backup = p; // correct deep copy, generated automatically
Playlist moved = std::move(p); // correct move, generated automatically
std::cout << backup.name() << ' ' << backup.size() << ' ' << moved.size() << '\n';
return 0;
}
Focus 2 2
Common mistakes with std::move
- Using an object after moving from it as if it still held its value.
return std::move(local);– it prevents copy elision. Justreturn local;.std::moveon aconstobject silently copies, because you can't steal from const.
Practice
Quick check
Does std::move move anything?
No – it's a cast to an rvalue reference; the move constructor or assignment does the moving.
What state is an object in after being moved from?
Valid but unspecified – you can assign to it or destroy it, but shouldn't rely on its value.
What is the rule of zero?
Design classes so members manage resources, and write no destructor or copy/move functions yourself.
Why mark move constructors noexcept?
So containers like vector use moves (not copies) when they reallocate.
Summary
- Moving transfers resources instead of copying; std::move marks an object as movable.
- Write move operations (noexcept) only when you manage a raw resource – then follow the rule of five.
- Prefer the rule of zero: build classes from vector, string and smart pointers.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.