1. Why std::unique_ptr is non-copyable
When you first see a std::unique_ptr, it's tempting to treat it like a raw pointer: "an address can be copied, so a unique_ptr should be copyable too." At the level of the address — yes, you can copy an address a hundred times. But the problem isn't the address, it's the responsibility: who is responsible for freeing the memory?
If two objects consider themselves owners of the same resource, eventually one of them will free the memory and the other will later try to free it again. That's the classic double-free — the bug that might not appear immediately, but "on Friday night in production" (developers joke that it's a special genre of horror). Therefore, for unique_ptr copying is not merely "discouraged", it is logically forbidden.
How C++ forbids copying
std::unique_ptr<T> is designed so that it cannot be copied: its copy constructor and copy assignment are deleted (deleted). That means: you won't even be able to accidentally "sneak" a copy through assignment — the compiler will stop you at build time.
And that's great news: it's better to get a compile error than to successfully build a program that sometimes crashes, sometimes corrupts memory, and sometimes pretends everything is fine. With unique_ptr the compiler acts as a strict guard: "a second owner shall not pass."
#include <memory>
int main() {
auto a = std::make_unique<int>(10);
auto b = a; // compilation error: copying is forbidden
}
Here auto b = a looks harmless, but semantically it means "now the resource has two owners". So the compiler will say something along the lines of: use of deleted function (the exact wording depends on the compiler).
2. Ownership transfer and std::move
If copying is not allowed, a practical question arises: "how then do you pass a resource between parts of a program?" For example, you created an object and then want to hand it to another variable, another function, or another logical unit.
The answer is move. Moving is not "creating another copy of the owner", it's "transferring ownership from one owner to another". The metaphor is simple: there are apartment keys (the resource) and there is the person who owns them (unique_ptr). Moving is giving the keys to another person. After that the first person no longer has the keys, but the person themself hasn't disappeared.
The scheme looks roughly like this:
flowchart LR
A["a: unique_ptr (owns)"] -->|"std::move(a)"| B["b: unique_ptr (becomes owner)"]
A2["a: unique_ptr (empty nullptr)"] --- B
And in code it looks like this:
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto a = std::make_unique<int>(10);
auto b = std::move(a); // transfer ownership
std::cout << std::boolalpha;
std::cout << (a == nullptr) << '\n'; // true
std::cout << *b << '\n'; // 10
}
Note: a after the transfer remains a valid object, but typically becomes empty (nullptr). This is normal and expected.
What std::move actually does
The name std::move is one of the most successful sources of confusion in C++ history. Beginners read "move" and think: "aha, it's a function that immediately transfers the data". But no: std::move(x) is semantically closer to the phrase "I allow x to be treated as an object that can be moved".
Technically std::move is a cast that turns an expression into an rvalue (roughly speaking, a "temporary/transferable value"). The actual transfer is not performed by std::move, but by the code that receives the result of std::move(x) and knows how to move.
A bad but very instructive example:
#include <memory>
#include <utility>
int main() {
auto p = std::make_unique<int>(5);
std::move(p); // by itself this does nothing: the result is not taken
// p still owns the resource
}
Here std::move(p); is practically meaningless. It only "asked" the compiler to treat p as movable, but no one accepted that movable value.
The correct usage of std::move is visible here:
#include <memory>
#include <utility>
int main() {
auto p = std::make_unique<int>(5);
auto q = std::move(p); // q took ownership
}
A small pedantic fact
The standard library carefully documents move behavior. So carefully, in fact, that editorial reports and lists of corrections to the standard contain specific points about the correctness of postconditions for move-assignment of unique_ptr, as well as guarantees like noexcept for moving operations.
This is a good signal: the topic is not "magical", it's strictly specified.
Move on assignment
Moving is not only "when creating a new variable" (auto b = std::move(a);), but also when assigning to an existing one. Here an important detail appears: what should happen to the resource that the left-hand unique_ptr owned before?
Answer: the left unique_ptr first releases its old resource, and then takes the resource from the right-hand side. So the operation is safe and self-contained: memory is not "lost" nor does it come to belong to two owners.
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto a = std::make_unique<int>(1);
auto b = std::make_unique<int>(2);
b = std::move(a); // b now owns what a used to own
std::cout << std::boolalpha;
std::cout << (a == nullptr) << '\n'; // true
std::cout << *b << '\n'; // 1
}
In this situation the "old" resource of b (where there was 2) is released automatically inside move-assignment. You don't write delete or manually "clean up" — RAII continues to work.
State after move
After std::move people often worry: "is the variable now broken?" It's not broken. It is valid, you can destroy it, assign a new value to it, compare it with nullptr. But there's one thing you must not do: do not dereference it until you've checked that it's not empty.
For unique_ptr the normal, expected moved-from status is nullptr. That's not an "error", it's an honest signal: ownership has moved.
Here is a mini "can/cannot" table for a moved-from unique_ptr:
| Action | Allowed? | Why |
|---|---|---|
|
yes | checking state is safe |
| assign a new resource | yes | unique_ptr will become an owner again |
| destroy (leave scope) | yes | destroying an empty owner is safe |
or |
no (without checking) | p may be nullptr |
Example of "correct discipline":
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto p = std::make_unique<int>(7);
auto q = std::move(p);
if (p) {
std::cout << *p << '\n';
} else {
std::cout << "p is empty\n"; // p is empty
}
std::cout << *q << '\n'; // 7
}
3. Practical: a draft task
To make ownership transfer less abstract, let's embed it into a small scenario. Imagine we have a console organizer app: we store tasks in a std::vector<Task>. Sometimes a user composes a draft task but hasn't added it to the list yet. The draft will either be accepted or discarded. This exactly fits the model of sole ownership.
Let's create Task and a variable draft, which is either empty or owns the draft:
#include <memory>
#include <string>
#include <vector>
struct Task {
int id{};
std::string title;
};
int main() {
std::vector<Task> tasks;
std::unique_ptr<Task> draft; // draft: either present or not
}
Now add creating a draft. We haven't discussed "ideal" function signatures for unique_ptr yet (that's for the next lecture), so we'll be as straightforward as possible: create the object and move it into draft.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct Task {
int id{};
std::string title;
};
int main() {
auto tmp = std::make_unique<Task>(Task{1, "Read about std::move"});
std::unique_ptr<Task> draft = std::move(tmp);
std::cout << (tmp == nullptr) << '\n'; // 1 (true)
std::cout << draft->title << '\n'; // Read about std::move
}
So tmp was a "temporary owner", then gave ownership to the draft. In real code a "temporary owner" may appear not because you wanted it, but because you received a unique_ptr from somewhere else and want to pass it along.
Now suppose we want to "accept the draft": add the task to tasks. Since tasks stores values of Task, not ownership by pointer, we simply copy/move the Task itself into the vector. It's useful for a beginner to see the difference: unique_ptr manages lifetime, while the vector stores the actual data.
#include <memory>
#include <string>
#include <vector>
struct Task {
int id{};
std::string title;
};
int main() {
std::vector<Task> tasks;
auto draft = std::make_unique<Task>(Task{1, "Finish lecture 212"});
tasks.push_back(*draft); // add the Task by value
draft = nullptr; // draft is "free" (the object held by draft will be deleted)
}
The line draft = nullptr; will release the resource owned by the owner. We intentionally use assignment to nullptr because more advanced management operations (reset, release, get) are topics for later lectures.
4. Typical mistakes with std::move and unique_ptr
When the concept of "move" clicks, there's a new temptation: start sprinkling std::move everywhere, as if it's a universal seasoning. But std::move is not a spice; it's a warning: "I'm going to give this value away now." So there are two kinds of mistakes: either you forget std::move where it's needed, or you put it where it breaks the logic.
Mistake #1: attempting to copy a unique_ptr out of habit.
This usually looks like auto b = a; or b = a;. The compiler will complain — and that's good. Fix is simple: if you really want to transfer ownership, use std::move(a). If you don't want to transfer ownership, then you probably don't need a unique_ptr at that point — you need non-owning access (we'll discuss that later).
Mistake #2: using a variable after std::move as if nothing happened.
After auto b = std::move(a); variable a often becomes nullptr. Dereferencing *a or a->field thereafter is almost guaranteed to crash. The habit should be: when you see std::move(a) — mentally cross out a as an owner and only use it after if (a) or after assigning a new resource to it.
Mistake #3: believing std::move frees memory by itself.
std::move does not delete or call delete. It only marks the expression as movable. Memory will be freed either inside the move operation of unique_ptr (when one owner replaces its resource), or when an owner is destroyed (goes out of scope), or when you explicitly make it empty (for example, p = nullptr).
Mistake #4: forgetting to include <utility> and being puzzled that std::move "doesn't exist".
This is a small but frequent thing: you included <memory> because you need unique_ptr, and it's natural to assume std::move is there too. But std::move lives in <utility>. If the compiler complains about unknown std::move, don't argue — just include the right header.
Mistake #5: trying to "move from a const".
If you have a const std::unique_ptr<T> p, that's an object that by contract must not be modified. Moving a unique_ptr changes the source (usually makes it empty). So you can't move from a const-owner. This commonly appears as an unexpected compile error where you wrote std::move(p) and it doesn't compile. The point is sensible: you cannot promise "I won't change" and at the same time "I'll give away ownership".
GO TO FULL VERSION