Lesson 9 of 12
Copying a large object is expensive. Often the source is a temporary that is about to be destroyed, so the copy is pure waste — we allocate a new array, duplicate every element, then throw the original away.
Moving takes the resource instead of duplicating it.
COPY MOVE
a: [ptr] --> [1 2 3] a: [ptr] --> [1 2 3]
b: [ptr] --> [1 2 3] (new) b: [ptr] ----^
a: [null] (a gives it up)
A copy duplicates the array. A move reassigns one pointer and leaves the source empty. For a million-element vector that is the difference between a million copies and one pointer write.
Writing them
#include <iostream>
#include <utility>
class Buffer {
int* data;
std::size_t count;
public:
Buffer(std::size_t n) : data(new int[n]), count(n) {
for (std::size_t i = 0; i < count; ++i) {
data[i] = 0;
}
}
~Buffer() { delete[] data; }
Buffer(const Buffer& other) : data(new int[other.count]), count(other.count) {
for (std::size_t i = 0; i < count; ++i) {
data[i] = other.data[i];
}
std::cout << " copy constructor\n";
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
count = other.count;
data = new int[count];
for (std::size_t i = 0; i < count; ++i) {
data[i] = other.data[i];
}
}
std::cout << " copy assignment\n";
return *this;
}
// Move constructor: take the pointer, leave the source empty but valid.
Buffer(Buffer&& other) noexcept : data(other.data), count(other.count) {
other.data = nullptr;
other.count = 0;
std::cout << " move constructor\n";
}
// Move assignment: release ours, take theirs, empty them out.
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
count = other.count;
other.data = nullptr;
other.count = 0;
}
std::cout << " move assignment\n";
return *this;
}
std::size_t size() const { return count; }
void set(std::size_t i, int v) { data[i] = v; }
int get(std::size_t i) const { return data[i]; }
};
int main() {
Buffer a(3);
a.set(0, 42);
std::cout << "copy:\n";
Buffer b = a;
std::cout << "move:\n";
Buffer c = std::move(a);
std::cout << "c[0]=" << c.get(0) << " c.size=" << c.size() << "\n";
std::cout << "a was moved from, a.size=" << a.size() << "\n";
return 0;
}
copy:
copy constructor
move:
move constructor
c[0]=42 c.size=3
a was moved from, a.size=0
c took a's array. a is now empty — still a valid object, still destructible, just holding
nothing.
&& — an rvalue reference
Buffer&& binds only to things that are about to expire: temporaries, and objects you explicitly
mark with std::move. That is how the compiler picks the move overload over the copy one.
#include <iostream>
void f(const int& x) { std::cout << "lvalue ref: " << x << "\n"; }
void f(int&& x) { std::cout << "rvalue ref: " << x << "\n"; }
int main() {
int a = 1;
f(a); // a has a name and persists -> lvalue
f(2); // a temporary -> rvalue
f(a + 1); // the result is a temporary -> rvalue
return 0;
}
lvalue ref: 1
rvalue ref: 2
rvalue ref: 2
Roughly: an lvalue has a name you could take the address of; an rvalue is a temporary. If the source is a temporary, stealing from it is safe, because nobody can observe it afterwards.
Two rules for the moved-from object
It must be left valid. Not correct, not useful — valid, meaning its destructor runs safely
and you may assign a new value to it. Setting data = nullptr matters: delete[] nullptr is
explicitly defined as a no-op, so the source's destructor is harmless.
You may not assume what it holds. After std::move(a), the only safe operations on a are
destroying it or assigning to it. Do not read it. For our Buffer we know it is empty; for a
std::string from the standard library the state is "valid but unspecified."
Why noexcept is not decoration
Buffer(Buffer&& other) noexcept
That keyword has real consequences. When std::vector grows, it must relocate its elements. It
will only move them if the move constructor is noexcept — otherwise it copies, because a throw
halfway through relocating would leave the vector unrecoverable.
So a move constructor that is not noexcept is silently ignored by the container that would
benefit most. Move operations do not allocate, so they have no reason to throw. Mark them
noexcept.
Where moves happen without you asking
#include <iostream>
#include <string>
#include <vector>
std::vector<std::string> build() {
std::vector<std::string> v;
v.push_back("alpha");
v.push_back("beta");
return v; // moved out, not copied
}
int main() {
std::vector<std::string> names = build();
std::cout << names.size() << " " << names[0] << "\n";
std::vector<std::string> more;
more.push_back(std::string("gamma")); // temporary is moved in
std::cout << more[0] << "\n";
return 0;
}
2 alpha
gamma
Returning a local by value, pushing a temporary into a container, passing a temporary to a function — all move automatically when a move constructor exists. You mostly benefit from move semantics without writing anything.
Try it
Add a counter to Buffer that records how many ints have been copied element-by-element.
Build a vector of several Buffers and compare the count with and without move operations.
Checkpoint
- A move transfers ownership of a resource instead of duplicating it.
T&&is an rvalue reference and binds only to temporaries andstd::moved objects.- The moved-from object must be left valid — destructible and assignable.
- After moving from an object, only destroy or reassign it; do not read it.
- Mark move operations
noexcept, or containers will copy instead of moving. - Returning locals and passing temporaries move automatically once you have the operations.
Questions about this lesson
Log in to ask a question.