Skip to content
VLSI Mentor

C++ Part I · Module 14 · Smart Pointers and Ownership

Ownership as an API Decision

Chapter 10.9 named ownership as the question that manual allocation cannot answer — which code is responsible for releasing this, and when — and observed that the only places available to record the answer were a comment, a naming convention, or documentation somewhere else, all three of which are unchecked and routinely false after a refactor. Modules 12 and 13 then built the machinery for a type to be an owner: a constructor that cannot be skipped, a destructor that runs on every path, and copy and move semantics that say deliberately what duplicating or transferring that owner means. What this module adds is the observation that ownership is not only an implementation detail of a class. It is a property of every function signature that accepts or returns an object, and the type system is expressive enough to state it — so that a caller reading the declaration knows whether they are handing something over, sharing it, or lending it, and cannot do the wrong one by accident. This chapter is the decision rather than the types: four questions to ask before writing a signature, the four forms that answer them, what each form measurably refuses, and the single mistake that none of them catches.

Intermediate17 min readC++Part IFundamentalsOwnership

Part I · Module 14 · Chapter 14.1 · Ownership as an API Decision

1. The Question, Moved to the Signature

Chapter 10.9 §2 posed ownership as a question about a resource:

Which code is responsible for releasing this, and when?

And Chapter 10.9 §6 measured the problem: the answer was conveyed in a comment (// caller must delete), a naming convention (createWidget versus getWidget), or documentation — all unchecked, and all false after the first refactor.

This module's premise is that the question belongs in the signature:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void consume(std::unique_ptr<Model> m);     // I take ownership
    void share(std::shared_ptr<Model> m);       // we share it
    void inspect(const Model& m);               // I only look
    Model* find(std::string_view name);         // ownership is elsewhere

Four declarations, four different answers, none of them in a comment.

2. Four Questions Before the Signature

Ask these in order. The first one that applies determines the form.

1. Does the function need the object to outlive the call? If no, it is an observer — go to question 4.

2. Should the caller still have it afterwards? If no, the function takes ownership. If yes, go to 3.

3. Do both genuinely need to keep it alive independently? If yes, ownership is shared. If no — and this is the common case — the caller keeps it and the function observes.

4. Can the object be absent? A reference cannot be null; a pointer can.

Chapter 10.9 §5 listed the three ownership patterns — exclusive, shared, borrowing — and noted that most pointers should be borrowing. These four questions are that observation turned into a procedure, and they land on borrowing most of the time.

3. The Forms

IntentSignatureWhat the caller must write
I take ownershipvoid f(std::unique_ptr<T> p)f(std::move(p)) — explicitly
We share ownershipvoid f(std::shared_ptr<T> p)f(p) — the count rises
I only read itvoid f(const T& t)f(obj) or f(*p)
I modify it in placevoid f(T& t)f(obj)
I read it, and it may be absentvoid f(const T* t)f(&obj) or f(nullptr)
I return a new objectstd::unique_ptr<T> f()the caller owns the result
I return a borrowed viewT* f() / const T& f()ownership stays with the callee

Chapter 13.7 §8 gave the four parameter forms as intent signals; this extends the same idea to ownership, and the extension is what the module blurb means by RAII plus a specific ownership policy.

4. Measured: What Each Form Does

Azvya Education Pvt. Ltd.VLSI Mentor
signatures.cpp — the five functions
static void takesOwnership(std::unique_ptr<Model> m);
static void sharesOwnership(std::shared_ptr<Model> m);
static void observesRef(const Model& m);
static void observesPtr(const Model* m);
static void mutatesRef(Model& m);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
observe, then still own:
    observesRef      id=7
    observesPtr      id=7
    mutatesRef       set id=99
    owner still holds id=99
share:
    sharesOwnership  id=7 use_count=2
    after the call, use_count=1
give it away:
    takesOwnership   id=99
    owned.get()=0x0

Three things to read off it.

The observers took nothing. After all three observer calls the owner still held the object — and mutatesRef even modified it, which an observer is allowed to do when the signature says T& rather than const T&.

Sharing is visible in the count. use_count was 2 inside the call and 1 after — the parameter was a copy of the shared pointer, and it released its share on return. Chapter 14.5 is what that count costs.

And giving it away empties the caller's pointer. owned.get() became 0x0. The object was destroyed when takesOwnership returned, which is the trace's takesOwnership id=99 line followed by nothing — the caller has no way to use it again, because it has nothing.

5. What the Caller Cannot Get Wrong

This is the clause the frozen description of this chapter emphasises, and it is measurable. Each case compiled separately:

CallResult
takesOwnership(std::move(u));OK
takesOwnership(u);error: call to implicitly-deleted copy constructor
observesRef(*u);OK
observesRef(local);OK

You cannot hand over ownership by accident. takesOwnership(u) does not compile, because a unique_ptr cannot be copied — Chapter 13.10 §3 measured that deletion and its diagnostic. To give the object away you must write std::move, which is Chapter 13.9 §1's framing: std::move is how a caller grants permission.

So the signature does two things at once. It tells the reader of the declaration what the function does with the object, and it forces the writer of the call to acknowledge it.

And the fourth row is worth noticing: an observer does not care how the object is owned. observesRef(local) works on a plain stack object, and observesRef(*u) works on one held by a unique_ptr. That is why observer signatures should never mention a smart pointer — Chapter 14.7 is that argument in full.

6. The Two Forms to Avoid

Two signatures look like they belong in §3's table and do not.

void f(const std::unique_ptr<T>& p) — a const reference to a unique pointer. This says I will not take ownership, but you must be storing this object in a unique_ptr specifically. The second half is a requirement with no purpose: the function only reads the object, so it should take const T& and work for callers who hold the object any way at all. This form needlessly narrows the interface.

void f(std::shared_ptr<T> p) when the function does not store it. Copying a shared_ptr costs an atomic increment and decrement (Chapter 14.5), and it tells the reader that the function intends to keep the object alive beyond the call. If it does not, const T& is both cheaper and more honest.

The rule behind both: mention a smart pointer in a parameter only if the function participates in ownership. Otherwise the parameter should name the object's type.

7. The One Thing No Signature Refuses

An honest limit, and it determines Chapter 14.4's advice.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Model local{1};
    takesOwnership(std::unique_ptr<Model>(&local));     // compiles

Compiler diagnostics: 0. And at runtime:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bad->id = 1
leaving the scope -- the destructor will call delete on &local
exit=134

"survived" never printed — exit 134 is SIGABRT, the allocator refusing a pointer it never issued. Under AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed
SUMMARY: bad-free unique_ptr.h:77 in default_delete<M>::operator()

So the type system enforces how many owners there are and cannot verify that what you handed it was heap-allocated. A unique_ptr will faithfully delete whatever address it was given, including the address of a stack object.

Two consequences:

This is why std::make_unique is the recommended way to construct one. It allocates and wraps in a single step, so there is no opportunity to hand it a pointer from somewhere else. Chapter 14.4 is that argument, and it has a second measured reason.

And a raw pointer appearing in a smart-pointer constructor is worth a second look in review. std::unique_ptr<T>(somePointer) is a claim that somePointer came from new and is not owned by anyone else, and neither half is checked.

8. Ownership in Return Types

The same decision applies on the way out, and it is easier because there are only two answers.

std::unique_ptr<T> create() — the caller owns the result. This is the factory form, and it is the honest replacement for the old T* create() whose contract lived in a comment (Chapter 10.9 §6). Chapter 13.12 §3 measured that returning it costs nothing: the guaranteed elision applies to a unique_ptr as to anything else.

T* find() or const T& at() — the caller gets a view and ownership stays where it was. The lifetime of the result is then the callee's business, and the caller must not outlive it, which Chapter 9.6 §5 measured going wrong for a string_view.

What is not a good return type is a raw T* that the caller is expected to delete. That is precisely Chapter 10.9 §6's unchecked comment, and unique_ptr exists to replace it.

9. Ownership in a Container

The same decision applies to the element type of a container, and it is where getting it wrong is most expensive — because the mistake is repeated once per element.

Three containers holding the same three objects, with a live counter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. std::vector<Model*>  -- raw pointers:
    3 elements, live=3
    after the vector was destroyed: live=3   <-- LEAKED

B. std::vector<std::unique_ptr<Model>>:
    3 elements, live=3
    after the vector was destroyed: live=0

C. std::vector<Model> -- objects by value:
    3 elements, live=3
    after the vector was destroyed: live=0

A vector<Model*> leaks every element. Chapter 10.6 §2 listed this as one of the four shapes of a leak, and it is worth restating precisely: destroying the vector destroys the pointers, which releases nothing. The vector did exactly its job; the elements were never owned by anything.

And the cost of fixing it is nothing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(Model*)                = 8
sizeof(unique_ptr<Model>)     = 8
sizeof(Model)                 = 4

A unique_ptr element is the same size as a raw pointer element. So vector<unique_ptr<Model>> stores the same eight bytes per slot, releases correctly, and makes the ownership visible in the declaration.

The three-way choice, in order of preference:

vector<T> — store the objects themselves. Smallest (4 bytes here rather than 8), one allocation for the whole run rather than one per element, and contiguous, which Chapter 9.3's 60× measurement says the hardware prefers. Use this unless something prevents it.

vector<unique_ptr<T>> — store owning pointers. Needed when the elements are polymorphic (Module 17), when they must not move as the vector grows, or when they are large and individually transferred. Chapter 14.3 §7 measures moving one out.

vector<T*> — store borrowed pointers to objects owned elsewhere. This is a legitimate and useful container, and Chapter 14.7 is its defence: a raw pointer element is a statement that ownership is somewhere else. It is a bug only when nothing else owns them, which is case A.

So case A is not "the wrong container type". It is the right container type with the ownership question unanswered — the same diagnosis Chapter 13.4 §9 gave for two raw owners of one buffer.

10. What to Actually Do

Answer §2's four questions before writing the signature.

Use const T& for an observer — it is the narrowest form and works regardless of how the caller owns the object.

Use std::unique_ptr<T> by value to take ownership, which forces the caller to write std::move.

Use std::shared_ptr<T> by value only when the function genuinely keeps the object alive (Chapter 14.5).

Use T* when the object may be absent, and T& when it may not.

Return std::unique_ptr<T> from a factory — never a raw owning pointer.

Never write const std::unique_ptr<T>& — take const T& instead.

Mention a smart pointer in a parameter only if the function participates in ownership.

And construct smart pointers with make_unique / make_shared, because §7 is not checked.

11. Mistakes Worth Making Once

Documenting ownership in a comment. Unchecked, and false after the first refactor (Chapter 10.9 §6).

Returning a raw owning pointer. The contract then lives nowhere the compiler can see.

Taking const std::unique_ptr<T>&. Narrows the interface for no benefit.

Taking std::shared_ptr<T> to read an object. An atomic increment and a false claim about lifetime.

Expecting takesOwnership(u) to work. Measured: error: call to implicitly-deleted copy constructor — and that refusal is the feature.

Wrapping a stack address in a unique_ptr. Measured: 0 diagnostics, then exit 134 and ASan bad-free.

Mentioning a smart pointer in an observer's parameter. It couples callers to a storage choice.

Writing a signature whose intent depends on a flag. Two functions.

12. Interview Relevance

"How do you express ownership in a C++ interface?" By the parameter and return types: unique_ptr by value takes ownership, shared_ptr by value shares it, const T& or T* observes, and a returned unique_ptr gives ownership to the caller. The answer that lands adds why — a signature is re-checked at every call, where a comment is not.

"What does void f(std::unique_ptr<T> p) tell the caller?" That the function takes ownership, and that they must write std::move to call it — which the compiler enforces, since the copy is deleted.

"Why not take const std::unique_ptr<T>&?" Because the function only reads the object, so it should take const T& and not constrain how the caller stores it.

"When is shared_ptr the right parameter type?" Only when the function keeps the object alive beyond the call. Otherwise it costs an atomic pair and misstates intent.

"Does the type system catch every ownership mistake?" No — std::unique_ptr<T>(&stackObject) compiles and aborts at runtime. It enforces how many owners there are, not where the object came from, which is why make_unique is recommended.

13. What You Should Be Able to Say Now

  1. Ownership belongs in the signature, not in a comment (Chapter 10.9 §6).
  2. Four questions: must it outlive the call; should the caller keep it; do both need it alive; can it be absent.
  3. Most answers land on borrowing (Chapter 10.9 §5).
  4. The forms: unique_ptr by value takes, shared_ptr by value shares, const T& reads, T& modifies, T* reads-or-absent, returned unique_ptr gives.
  5. Measured: observers left the owner owning — mutatesRef changed the value and the owner still held it.
  6. Measured: sharesOwnership raised use_count to 2 during the call and back to 1 after.
  7. Measured: takesOwnership(std::move(u)) left u.get() at 0x0.
  8. Measured: takesOwnership(u) does not compile — implicitly-deleted copy constructor. The caller cannot give ownership away by accident.
  9. An observer does not care how the object is owned — observesRef took a stack object and a unique_ptr's target.
  10. Avoid const std::unique_ptr<T>& and shared_ptr parameters for functions that only read.
  11. Measured limit: std::unique_ptr<M>(&local) compiles with 0 diagnostics and aborts with exit 134; ASan reports bad-free.
  12. The type system records how many owners, not where the object came from — hence make_unique (Chapter 14.4).
  13. Return unique_ptr from factories; never a raw owning pointer.
  14. A signature is re-checked at every call; a comment is checked never.

14. Exercises

Exercise 1 — choose the form. Write signatures for: a function that prints a model; one that stores a model in a registry for later; one that modifies a model in place; one that creates a model; one that finds a model in a registry and may fail.

Exercise 2 — the four questions. Apply them to void analyse(???) where the function reads a 10 MB buffer and returns a summary, and say which form you land on.

Exercise 3 — predict. With void take(std::unique_ptr<M>); which compile: take(u);, take(std::move(u));, take(std::make_unique<M>());, take(u.get());

Exercise 4 — fix the signatures. Improve each: void print(const std::unique_ptr<M>& m);, void log(std::shared_ptr<M> m); (it only reads), M* create();

Exercise 5 — the limit. Explain what std::unique_ptr<M>(&localObject) does, what diagnostics you get, and which chapter's advice prevents it.

Exercise 6 — observer reuse. Explain why void inspect(const M&) is more reusable than void inspect(const std::unique_ptr<M>&), with reference to the measurement.

Exercise 7 — the comment. A header says // caller owns the returned pointer. Rewrite the declaration so the comment is unnecessary, and say what the compiler now enforces.

Exercise 8 — shared or not? For each, decide whether shared_ptr is justified: a cache shared by three subsystems; a model read by one function; a node in a tree with parent links; a config object alive for the program's duration.

15. Summary

  • Ownership is a property of a signature, not a comment — and a signature is re-checked at every call.
  • Four questions decide the form, and most answers are borrowing.
  • Measured: observers leave the owner owning; sharing shows as use_count 2 → 1; taking leaves the caller's pointer 0x0.
  • Measured: takesOwnership(u) is a compile error — the caller must write std::move, so ownership cannot be given away by accident.
  • An observer should name the object's type, not a smart pointer — it then works for any caller.
  • Avoid const unique_ptr<T>& and shared_ptr parameters that do not participate in ownership.
  • Measured limit: wrapping a stack address compiles with 0 diagnostics and aborts (exit 134, ASan bad-free).
  • The type system enforces how many owners, not provenance — hence make_unique.
  • Factories return unique_ptr.

The decision procedure is in place and one type has done all the work in it without being introduced. Every signature above that takes or returns ownership is written in terms of a single class template that has appeared in this curriculum since Chapter 10.9 named it as the answer and has never been explained: what it is, what it costs, and why it rather than the shared one should be the thing you reach for first. The next chapter is that type, and the two measurements worth having in advance are that it is exactly the size of the pointer it holds, and that a function using one locally compiles to byte-identical instructions to the same function written with a raw pointer and a hand-written delete.

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.