C++ Part I · Module 14 · Smart Pointers and Ownership
std::unique_ptr: Exclusive Ownership
Chapter 10.9 named ownership as the question that manual allocation cannot answer, and said the standard library contains a type whose entire purpose is to be the answer for the common case. Chapter 12.9 then built that type by hand — acquire in a constructor, release in a destructor, copy operations deleted — and called the result honest but restricted. Chapter 13.11 argued that holding such a type as a member is why most well-written classes need none of the five special functions. This chapter is the library's version, which is what all of that was pointing at: a class template holding exactly one pointer, releasing it in its destructor, and refusing to be copied so that a second owner cannot come into existence. The three claims worth checking are that it has one owner, that it costs nothing, and that the release is automatic, and all three are measurable. The size is the size of the pointer it holds, with nothing added, even for an array or a large element type. The generated code for a function that creates, uses and destroys one is byte-for-byte the same as the equivalent written with new and a hand-placed delete. And attempting to make a second owner is a compile error rather than the double free that Chapter 13.4 traced.
Intermediate17 min readC++Part IFundamentalsOwnership
Part I · Module 14 · Chapter 14.2 · std::unique_ptr: Exclusive Ownership
1. What It Is
#include <memory>
auto model = std::make_unique<ReferenceModel>(config);
std::unique_ptr<T>holds a pointer to one object, releases it in its destructor, and cannot be copied.
That is the whole type. It is Chapter 12.9's RAII with one specific ownership policy — exclusive — and Chapter 10.9 §5 identified exclusive as by far the common case.
2. The Operations
auto p = std::make_unique<Model>(args); // create and own
p->field; // use it, like a pointer
(*p).field; // the same
Model* raw = p.get(); // look at the pointer without taking it
if (p) { /* ... */ } // is it holding anything?
p.reset(); // release now
p.reset(new Model); // release and take a different one
Model* taken = p.release(); // give up ownership, return the pointerTwo of those deserve care and get it later: get() is Chapter 14.7's subject, and release() is the one operation that hands you back an unowned raw pointer, so it is where ownership leaves the type system again.
(Every listing here uses std::make_unique, which Chapter 14.4 explains and which Chapter 14.1 §7 already gave one reason to prefer.)
3. Released Automatically
void process()
{
auto model = std::make_unique<Model>(4);
if (bad) return; // released
if (worse) throw std::runtime_error("x"); // released
// ...
} // releasedNothing in that function releases anything, and the object is released on all three paths. This is Chapter 12.9's measurement — 8 acquisitions, 8 releases, 0 still live across five exit paths including an exception — with the hand-written Handle replaced by a library type.
So unique_ptr is not a new mechanism. It is the mechanism of Module 12 packaged, and everything Chapter 12.9 §2 established about why it works applies unchanged: a constructor that cannot be skipped, a destructor that runs on every path, and destruction in reverse order.
4. One Owner — Measured
Each case compiled separately:
| Code | Result |
|---|---|
auto b = std::move(a); | OK |
auto b = a; | error: call to implicitly-deleted copy constructor |
std::unique_ptr<int> b; b = a; | error |
std::unique_ptr<int> b; b = std::move(a); | OK |
int* raw = a.get(); | OK |
int v = *a; | OK |
a.reset(); | OK |
The copy is deleted, and the move is allowed. That is the mechanism Chapter 13.10 §2 measured: unique_ptr declares a move constructor, which deletes the copy operations — and Chapter 13.1 §8 measured that rule arriving three notes deep in a diagnostic when a struct holding one could not be copied.
The consequence is the point of the type:
A second owner cannot come into existence, so Chapter 13.4's two-objects-one-buffer arrangement is unexpressible.
Chapter 13.4 traced that arrangement to exit 133. Here the equivalent mistake is a compile error at the line that makes it.
5. Zero Overhead — Measured
Two measurements, and the second is the one that matters.
Size. unique_ptr holds a pointer and adds nothing:
sizeof(int*) = 8
sizeof(std::unique_ptr<int>) = 8
sizeof(std::unique_ptr<Big>) = 8 (Big is 256 bytes)
sizeof(std::unique_ptr<int[]>) = 8
sizeof(std::shared_ptr<int>) = 16Eight bytes regardless of the element type, and regardless of whether it manages an array. Chapter 8.1 measured a raw pointer at 8 bytes; this is the same.
Generated code. Two functions that each allocate an int, dereference it, and release it — one with new/delete, one with make_unique:
int localRaw(int n) { int* p = new int(n); int r = *p + 1; delete p; return r; }
int localUnique(int n) { auto p = std::make_unique<int>(n); return *p + 1; }At -O2:
localRaw: add w0, w0, #1
ret 2 instructions
localUnique: add w0, w0, #1
ret 2 instructionsByte-identical. The optimiser removed the allocation entirely in both cases, and what remains is the arithmetic. There is no unique_ptr left in the generated code — the abstraction compiled away.
6. One Honest Qualification
"Zero overhead" is usually stated flatly, and there is one place it needs a footnote.
A by-value unique_ptr parameter is not passed in a register the way a raw pointer is. It is a class type with a destructor, so this platform's calling convention passes it indirectly — in memory, with the callee loading the pointer out. So:
int rawDeref(int* p) { return *p + 1; } // ldr w8,[x0]; add; ret
int uniqueDeref(std::unique_ptr<int> p) { return *p + 1; } // ldr x8,[x0]; ldr w8,[x8]; add; retOne extra load at that boundary. It is not because the type is fat — sizeof is 8 either way — but because of how class types with destructors are passed.
Two things follow, and neither is a reason to avoid the type:
It does not apply to local use, which is §5's measurement and the overwhelmingly common case.
And a by-value unique_ptr parameter means take ownership (Chapter 14.1 §3), which is a transfer of responsibility — if that costs one load, the load is not the interesting part of what just happened.
(A method note: my first attempt at §5 compared int* against const unique_ptr<int>& and found an extra load, which I nearly reported as overhead. It was the reference indirection — an unfair comparison, since one parameter was the pointer and the other was a reference to a wrapper around it. The local-object test in §5 is the fair one.)
7. Looking Without Taking
Model* raw = p.get(); // the pointer, ownership unchanged
Model* own = p.release(); // the pointer, and p now owns nothingThe distinction matters:
get() is a borrow. The unique_ptr still owns the object and will still release it. The returned pointer is an observer — Chapter 14.7's subject — and must not outlive the unique_ptr.
release() is a transfer out of the type system. After it, nothing owns the object and you are responsible for deleting it by hand. Chapter 10.9's question is live again, with all of Module 10's consequences.
So release() is rare and deliberate: handing a pointer to a C API that will free it, or to code that cannot be changed. If you find yourself calling it to pass the object somewhere, the signature of that somewhere is probably wrong — Chapter 14.1 §3's table has the forms.
And get() is for passing the object to an observer whose parameter is const T& or T*. Chapter 14.1 §5 measured that such a function works identically on a stack object, so the smart pointer never needs to appear in its signature.
8. As a Member
Chapter 13.11 §4 measured the consequence and it is worth restating here, because it is why unique_ptr matters beyond local variables:
class Simulation {
public:
explicit Simulation(const Config& c) : model_(std::make_unique<ReferenceModel>(c)) {}
// no destructor, no copy operations, no move operations
private:
std::unique_ptr<ReferenceModel> model_;
};Simulation needs none of the five special functions. The destructor is generated and releases the model; the copy operations are deleted automatically, because a unique_ptr member cannot be copied; and the move operations are generated and transfer the pointer.
Chapter 13.1 §8 measured this: a struct holding a std::unique_ptr has copy-constructible = 0, and the error chain names the reason. So composition with a unique_ptr produces a move-only type with no declarations at all — Chapter 13.10 §8's second coherent shape, obtained free.
That is Chapter 13.11's Rule of Zero applied to a resource that has no self-managing container: vector and string handle their own cases, and unique_ptr handles the general one.
9. Why Before shared_ptr
The frozen description of this chapter says unique_ptr is what to reach for before shared_ptr, and there are four reasons.
It is smaller — 8 bytes against 16, measured in §5.
It is cheaper. No reference count to maintain, which Chapter 14.5 measures as an atomic operation per copy. Here a transfer is a pointer assignment.
It allocates less. Chapter 14.5 §2 measures shared_ptr<T>(new T) as two allocations against make_unique's one.
And — the real reason — it states something stronger. Chapter 10.9 §5 observed that shared ownership is "the one most often chosen to avoid making a decision". A unique_ptr in a signature says exactly one piece of code is responsible for this; a shared_ptr says several might be, and the last one out releases it. The second is a weaker claim, harder to reason about, and correct only when the lifetime genuinely has several independent stakeholders.
So the order is: unique_ptr by default; shared_ptr when you can name the stakeholders and say why each must keep the object alive. Chapter 14.5 is that case, with its costs measured.
10. The Limit
Chapter 14.1 §7's measurement belongs here too, because it is the one thing this type does not check:
Model local{1};
std::unique_ptr<Model> bad(&local); // 0 diagnosticsexit=134
ASan: attempting free on address which was not malloc()-ed
SUMMARY: bad-free unique_ptr.h:77 in default_delete<M>::operator()unique_ptr enforces how many owners there are and cannot verify where the pointer came from. It will delete whatever address it holds.
Which is the second argument for make_unique: it allocates and wraps in one step, so there is no pointer to supply wrongly. Chapter 14.4 has the first argument, which is about exception safety and is measured.
11. What to Actually Do
Reach for std::unique_ptr by default when an object must outlive its scope.
Create it with std::make_unique — never unique_ptr<T>(new T) (Chapter 14.4).
Do not allocate at all if a plain local would do (Chapter 10.1 §7).
Pass it by value to transfer ownership, which forces the caller to write std::move (Chapter 14.3).
Pass const T& or T* to observers — use get(), and never put the smart pointer in an observer's signature.
Hold it as a member to get a move-only type with no special functions written (Chapter 13.11).
Avoid release() unless handing the pointer to code you cannot change.
Prefer shared_ptr only when you can name the stakeholders (Chapter 14.5).
And never wrap a pointer you did not allocate — measured: 0 diagnostics, then exit 134.
12. Mistakes Worth Making Once
Wrapping a stack address. Measured: 0 diagnostics, exit 134, ASan bad-free.
Using a unique_ptr where a local object would do. An allocation and an indirection for nothing.
Expecting auto b = a; to work. error: call to implicitly-deleted copy constructor — and that is the type working.
Calling release() to pass the object somewhere. The receiving signature is probably wrong.
Letting a get() pointer outlive the unique_ptr. Chapter 10.7's use-after-free.
Putting const std::unique_ptr<T>& in a signature. Chapter 14.1 §6.
Reaching for shared_ptr first. Twice the size, an atomic per copy, two allocations, and a weaker claim.
Assuming "zero overhead" covers every context. A by-value parameter is passed indirectly — one extra load (§6).
13. Interview Relevance
"What is std::unique_ptr?" A move-only smart pointer holding one object and releasing it in its destructor — RAII with exclusive ownership. The measurements that land are sizeof equal to a raw pointer and the copy being deleted.
"What does it cost?" Nothing for local use — measured byte-identical generated code to hand-written new/delete. The honest footnote is that a by-value parameter is passed indirectly, costing one load.
"Why can't you copy one?" Because two owners of one object is Chapter 13.4's double free. It declares a move constructor, which deletes the copies — so the mistake is a compile error.
"When would you use shared_ptr instead?" When the lifetime genuinely has several independent stakeholders you can name. Otherwise unique_ptr: smaller, cheaper, fewer allocations, and a stronger statement.
"Does it prevent all ownership bugs?" No — it will delete whatever pointer it is given, so wrapping a stack address compiles and aborts. That is why make_unique is recommended.
"What does a unique_ptr member do to the enclosing class?" Makes it move-only with no special functions written — the Rule of Zero.
14. What You Should Be Able to Say Now
std::unique_ptr<T>holds one object, releases it in its destructor, and cannot be copied.- It is Chapter 12.9's RAII with the exclusive ownership policy — nothing new, packaged.
- Released on every path, including exceptions — Chapter 12.9 measured 8 of 8.
- Measured: the copy is
implicitly-deleted; the move is allowed;get(),*p,reset()all work. - So a second owner is unexpressible — Chapter 13.4's exit 133 becomes a compile error.
- Measured size: 8 bytes, equal to a raw pointer, for
int, for a 256-byte type, and for an array form;shared_ptris 16. - Measured codegen:
localRawandlocalUniqueboth compile to 2 identical instructions at-O2. - Honest qualification: a by-value parameter is passed indirectly, costing one extra load — not fatness.
get()borrows;release()hands the pointer out of the type system.- As a member it gives a move-only type with no special functions (Chapters 13.1 §8, 13.11).
- Prefer it to
shared_ptr: 8 vs 16 bytes, no atomic, one allocation vs two, and a stronger claim. - Measured limit: wrapping a stack address gives 0 diagnostics then exit 134 — it enforces owner count, not provenance.
- Hence
make_unique(Chapter 14.4). - Do not allocate at all when a local would serve (Chapter 10.1 §7).
15. Exercises
Exercise 1 — convert it. Rewrite Model* m = new Model(c); use(m); delete m; with unique_ptr, and say how many lines you removed and what paths are now covered.
Exercise 2 — predict. With auto a = std::make_unique<int>(1); which compile: auto b = a;, auto b = std::move(a);, int* r = a.get();, int* r = a.release();, a.reset();, if (a) {}
Exercise 3 — the size claim. Explain why sizeof(std::unique_ptr<Big>) is 8 when Big is 256 bytes, and what that tells you about where the object lives.
Exercise 4 — the codegen claim. State what the byte-identical result shows, and say which comparison would have been unfair and why.
Exercise 5 — get or release? For each, choose: passing the object to void print(const Model&); handing it to a C API that calls free; storing a pointer in an observer list; returning it from a factory.
Exercise 6 — as a member. A class holds a unique_ptr<Model>. List the five special functions and say what each is for that class.
Exercise 7 — the limit. Explain what happens with std::unique_ptr<Model>(&localModel) at compile time and at run time, and give the habit that prevents it.
Exercise 8 — not the answer. Give three situations where unique_ptr is the wrong choice and say what to use instead.
16. Summary
std::unique_ptr<T>: one owner, released in its destructor, cannot be copied — RAII with the exclusive policy.- Measured: the copy is deleted, so a second owner is unexpressible and Chapter 13.4's exit 133 becomes a compile error.
- Measured: 8 bytes, equal to a raw pointer, for every element type including arrays;
shared_ptris 16. - Measured: byte-identical generated code to hand-written
new/delete— 2 instructions each. - Honest footnote: a by-value parameter is passed indirectly, one extra load.
get()borrows;release()leaves the type system.- As a member it yields a move-only type with no special functions written.
- Prefer it to
shared_ptron size, cost, allocations, and the strength of the claim. - Measured limit: it will
deleteany pointer given to it — exit 134 for a stack address — hencemake_unique. - And do not allocate at all when a local would do.
The type is established and so is the rule that a second owner cannot exist, which raises an obvious practical question this chapter has sidestepped. If a unique_ptr cannot be copied, how does an object get from the function that created it to the one that will keep it — out of a factory, into a container, from one subsystem to another — without either duplicating the pointer or dropping the object on the floor? The answer is the mechanism Module 13 spent six chapters building, used for the purpose it was designed for, and it gives a signature taking a unique_ptr by value a reading that is worth saying out loud: the words you now own this. The next chapter is that transfer, measured in and out of functions and containers, including the one place where moving an element out leaves something behind.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
