Skip to content
VLSI Mentor

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

Transferring Ownership with unique_ptr

If a unique_ptr cannot be copied, there is an obvious practical question that the previous chapter raised and did not answer: how does an object get from the function that created it to the one that will keep it? A factory makes a model and the simulation has to end up holding it. A parser produces a transaction and a scoreboard has to take it. A subsystem being shut down has to hand its resources to whatever outlives it. None of those can duplicate the pointer, because duplication is exactly what the type forbids, and none of them can afford to drop the object. The answer is the mechanism Module 13 spent six chapters building, used for the purpose it was designed for — and the result is that every hand-over in a codebase using unique_ptr is written explicitly at the point it happens, because the compiler refuses the alternative. This chapter traces one object through seven stages of that: produced by a factory, observed without being taken, handed to a function that consumes it, put into a container, taken back out, and finally destroyed. Two of the seven are worth knowing in advance — the callee's destructor runs before the caller's next statement, and taking an element out of a vector does not remove it.

Advanced17 min readC++Part IFundamentalsOwnership

Part I · Module 14 · Chapter 14.3 · Transferring Ownership with unique_ptr

1. The Only Way to Hand It Over

Chapter 14.2 §4 measured the copy as deleted, so there is exactly one way to move ownership from one unique_ptr to another:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto b = std::move(a);          // b owns it; a owns nothing

Chapter 13.9 §3 established what std::move actually is — a static_cast to T&&, producing an rvalue expression that selects the move constructor — and Chapter 13.9 §2 measured that it moves nothing by itself. Here the receiving operation is unique_ptr's move constructor, which takes the pointer and nulls the source.

The practical consequence is the one the frozen description of this chapter emphasises: every transfer is written down. There is no way to hand ownership over accidentally, because the only syntax that does it says so.

2. Reading the Signature

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void consumes(std::unique_ptr<Model> m);

A by-value unique_ptr parameter means: you now own this.

Not "I might keep it" and not "I will look at it". The function received the only owner, so whatever happens to the object is now the callee's business — and if the callee does nothing in particular, the object is destroyed when it returns.

Chapter 14.1 §5 measured the enforcement: consumes(a) does not compile, and consumes(std::move(a)) does. So the caller is made to acknowledge the transfer at the call site.

3. The Trace

One object through seven stages.

Azvya Education Pvt. Ltd.VLSI Mentor
transfer.cpp — the functions
static void consumes(std::unique_ptr<Model> m) { /* owns it now */ }
static void observes(const Model& m)           { /* only reads */ }
static void observesPtr(const Model* m)        { /* only reads, may be null */ }
static std::unique_ptr<Model> produces()       { return std::make_unique<Model>("produced"); }

Measured, with + on construction and - on destruction:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. produce and own:
    + produced
   a holds produced, a.get()=0x100629940
2. observe without owning:
    observes got produced
    observesPtr got produced
   a still holds produced
3. hand ownership to a function (by value + std::move):
    consumes got produced
    - produced
   a.get() is now 0x0  (a owns nothing)
4. into a container:
    + in-vector-1
    + in-vector-2
   vector holds 2 models
5. out of a container:
   taken=in-vector-1, v[0].get()=0x0, v.size()=2
6. clearing the vector:
    - in-vector-2
   done; taken still holds in-vector-1
7. leaving main:
    - in-vector-1

4. Out of a Factory

Step 1. produces() returns a unique_ptr by value and the caller owns the result:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static std::unique_ptr<Model> produces() { return std::make_unique<Model>("produced"); }
    auto a = produces();

Two things about this, and both matter for whether you use the pattern.

It costs nothing. Chapter 13.12 §3 measured that returning a temporary is guaranteed free in C++17 — 0 copies and 0 moves, unchanged under -fno-elide-constructors. A unique_ptr is no exception, so a factory returning one is not paying for the privilege.

And it replaces the old form whose contract lived in a comment. Chapter 10.9 §6 measured the three ways ownership used to be conveyed — a comment, a naming convention, documentation — all unchecked. std::unique_ptr<Model> produces() says the same thing in the return type, and Chapter 14.1's callout explained why that matters: a signature is re-checked at every call.

5. Into a Function That Consumes It

Step 3, and it is the chapter's central measurement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3. hand ownership to a function (by value + std::move):
    consumes got produced
    - produced
   a.get() is now 0x0  (a owns nothing)

Two facts in three lines.

The object was destroyed inside the callee. The - produced line appears before the caller's next statement. consumes received the only owner, its parameter went out of scope when it returned, and the destructor ran. If consumes had stored the pointer somewhere, the object would have survived; because it did not, the object's life ended there.

And the caller's pointer is empty. a.get() is 0x0 — not dangling, not stale, null. Chapter 13.9 §7 measured that unique_ptr is the one standard type whose moved-from state is guaranteed rather than merely unspecified, precisely because null is the only state in which it can safely be destroyed.

That guarantee is what makes the transfer safe to reason about: after std::move(a), a is testable (if (a) is false) and destroying it does nothing. Chapter 13.9 §6's assign to it or destroy it, do not read it applies to most types; for unique_ptr you may also test it.

6. Dropping It Versus Keeping It

§5's callee did nothing with the object, so it was destroyed on return. That is one of two things a consuming function can do, and the distinction is invisible in the signature — both are void f(std::unique_ptr<Model>). Measured side by side:

Azvya Education Pvt. Ltd.VLSI Mentor
sink.cpp — drop, then store
static void drops(std::unique_ptr<Model>) {}            // parameter dies at return

class Scoreboard {
public:
    void add(std::unique_ptr<Model> m) { models_.push_back(std::move(m)); }
private:
    std::vector<std::unique_ptr<Model>> models_;
};
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. consumer that drops:
    + dropped
  calling drops(std::move(a))
    - dropped
  returned; a.get()=0x0
B. sink that stores:
    + stored
  calling sb.add(std::move(b))
  returned; b.get()=0x0, sb.size()=1
  the local b is gone; object still alive? sb.size()=1
  leaving the scoreboard's scope
    - stored
C. done

Read the two blocks against each other. In A the destructor line sits between the call and the return — the object was gone before the caller resumed. In B there is no destructor line at the call at all; b.get() is 0x0 exactly as in A, the local b then goes out of scope with nothing happening, and - stored appears only when the Scoreboard itself is destroyed.

So the caller's side is identical in both cases — it gave the object away and holds null — and the object's lifetime is entirely the callee's decision. That is what taking ownership means, and it is why the signature does not need to distinguish the two: you now own this already covers both "and I destroyed it" and "and I kept it for twenty minutes".

The mechanism inside add is worth noticing: the parameter m is a named unique_ptr, so it is an lvalue, so storing it needs std::move again. Chapter 13.7 §6 measured that trap — inside a function, a named parameter is an lvalue, so handing it on selects the copying overload — and Chapter 13.9 §4 lists passing on such a parameter as one of the three places std::move belongs. For a unique_ptr the copying overload does not exist, so the omission is caught:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to implicitly-deleted copy constructor of 'std::unique_ptr<Model>'

Writing models_.push_back(m) fails to compile — the same protection as §2's, applied one level in. For a std::string member the identical omission would have compiled and silently copied.

7. Observing Without Taking

Step 2 is the control, and it is the common case rather than the exception:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    observes(*a);          // const Model&
    observesPtr(a.get());  // const Model*
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a still holds produced

Neither function took anything. Chapter 14.1 §5 measured that these signatures work identically on a stack object, which is why an observer's parameter should never mention a smart pointer.

So the division in practice is:

IntentCall
read itobserves(*p) or observesPtr(p.get())
take itconsumes(std::move(p))

And the visual difference at the call site is the point: std::move is the marker that ownership changed hands. Reading a function body, every std::move on a unique_ptr is a place where something was given away.

8. Into and Out of a Container

Step 4 is unremarkable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::vector<std::unique_ptr<Model>> v;
    v.push_back(std::make_unique<Model>("in-vector-1"));

The argument is an rvalue — Chapter 13.6 §4's classification — so it is moved into the container with no copy. Chapter 14.1 §9 measured that such a container releases its elements correctly where vector<Model*> leaked all three.

Step 5 is the one worth knowing in advance:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto taken = std::move(v[0]);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   taken=in-vector-1, v[0].get()=0x0, v.size()=2

v.size() is still 2. Moving out of a container does not remove the element — it leaves an empty unique_ptr in the slot. The vector still has two elements; one of them now owns nothing.

That is correct and it surprises people. A vector has no idea you consider the slot finished with; all you did was move out of the object stored there, which is Chapter 13.9's moved-from state applied to an element. Step 6 confirms the consequence — v.clear() destroyed only in-vector-2, because v[0] had nothing left to destroy.

So the two steps are separate, and if you want the slot gone you must say so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto taken = std::move(v[0]);
    v.erase(v.begin());                 // now size() is 1

erase and std::remove_if are Module 19 and Module 20's subjects; the point here is that taking ownership and removing the element are two operations. Forgetting the second leaves a container of empty pointers, which is not a leak — the objects were transferred, not lost — but it is a container whose size() lies about how many models it holds, and a loop over it will dereference a null unique_ptr unless it checks.

9. Returning It Back Out

The symmetry worth having: a function can hand ownership back the same way it received it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::unique_ptr<Model> Registry::take(std::string_view name)
    {
        auto it = find(name);
        auto out = std::move(it->second);     // the registry's slot is now empty
        entries_.erase(it);                   // and the slot is gone
        return out;                           // the caller owns it
    }

Three things in that function, each from a different chapter: the move empties the stored pointer (§8), the erase removes the slot (§8), and the return costs nothing (Chapter 13.12 §4 — a named local is elided in practice and a move at worst).

And the name is doing work. Chapter 13.9 §9 argued that a function moving out of a member should be named honestly — take, release, extract — because it changes the object's state. Registry::get returning a unique_ptr would be a lie; Registry::take is not.

10. The Four Transfers

Collecting them:

DirectionForm
out of a factorystd::unique_ptr<T> make(); — caller owns the result
into a functionvoid consume(std::unique_ptr<T> p); called with std::move
into a containerv.push_back(std::make_unique<T>(...)) or v.push_back(std::move(p))
out of a containerauto p = std::move(v[i]); plus v.erase(...)

Every one of them is visible in the source, which is the property Chapter 14.1's callout identified as the real difference from a documented convention.

11. What to Actually Do

Transfer with std::move — it is the only way, and that is the feature.

Read a by-value unique_ptr parameter as you now own this.

Return unique_ptr from factories — measured free (Chapter 13.12 §3).

Pass const T& or T* to observers, never the smart pointer.

After moving out of a container, erase the slot — the two operations are separate.

Name transferring functions honestly — take, not get.

Treat a fight with unique_ptr as information about the ownership model.

And test a moved-from unique_ptr if you need to — null is guaranteed (Chapter 13.9 §7).

12. Mistakes Worth Making Once

Expecting consume(p) to compile. It does not, and that refusal is the point.

Using an object after moving it away. p.get() is 0x0 — measured.

Assuming moving out of a vector removes the element. Measured: v.size() is still 2, with an empty slot.

Looping over a container of moved-from elements without checking. A null dereference.

Calling release() to pass the object somewhere. The receiving signature is wrong (Chapter 14.2 §7).

Naming a transferring function get. It changes the object's state.

Reaching for shared_ptr because several subsystems use the object. That is observation, not shared ownership.

Putting a smart pointer in an observer's parameter. Chapter 14.1 §6.

13. Interview Relevance

"How do you pass a unique_ptr to a function?" By value with std::move if the function takes ownership; by const T& or T* (via get()) if it only observes. The answer that distinguishes names which one the signature should be rather than reaching for one.

"What does a by-value unique_ptr parameter mean?" That the function takes ownership — and the caller must write std::move, which the compiler enforces.

"What is the state of the caller's pointer afterwards?" Null, guaranteed — unique_ptr is the one standard type with a specified moved-from state, so it can be tested.

"What happens when you move a unique_ptr out of a vector?" The element remains as an empty pointer and size() is unchanged. Taking ownership and erasing are two operations. This is a good discriminator.

"Does returning a unique_ptr from a factory cost a move?" No — guaranteed elision for a returned temporary (C++17).

14. What You Should Be Able to Say Now

  1. std::move is the only way to transfer a unique_ptr, so every hand-over is written down.
  2. A by-value unique_ptr parameter means you now own this.
  3. Measured: consumes(std::move(a)) destroyed the object inside the callee, before the caller's next statement.
  4. Measured: the caller's a.get() became 0x0 — null, and guaranteed so (Chapter 13.9 §7), hence testable.
  5. Measured: observers took nothing — a still holds produced after two observer calls.
  6. std::move at a call site is the marker that ownership changed hands — greppable.
  7. Returning from a factory costs nothing — guaranteed elision (Chapter 13.12 §3).
  8. Into a container: the argument is an rvalue, so it moves; the container then releases correctly (Chapter 14.1 §9).
  9. Measured: moving out of a container left v[0].get() at 0x0 with v.size() still 2.
  10. Taking ownership and erasing the slot are two operations — erase is needed for the second.
  11. A container of moved-from elements has a size() that misleads and will null-dereference an unchecked loop.
  12. Name transferring functions honestly — take, not get (Chapter 13.9 §9).
  13. unique_ptr rules out "keep it and give it away" — if you need that, the model is shared (Chapter 14.5).
  14. A fight with the type is information, not an inconvenience.

15. Exercises

Exercise 1 — the four transfers. Write one line for each of §10's four directions, using a Model type.

Exercise 2 — predict the trace. A factory produces a model; it is observed twice; it is passed to a consuming function; the caller then prints p.get(). Give the output order including constructor and destructor lines.

Exercise 3 — the container surprise. For a vector<unique_ptr<Model>> with three elements, after auto x = std::move(v[1]); say what v.size() is, what v[1] holds, and what v.clear() destroys.

Exercise 4 — fix the loop. for (auto& p : v) p->run(); after one element was moved out. Say what happens and give two fixes.

Exercise 5 — name it. A registry function removes an entry and hands the object to the caller. Give the signature and a good name, and say why get would be wrong.

Exercise 6 — observe or take? For each, choose the signature: a function that logs a model's name; one that stores the model in a scoreboard; one that validates a model and returns a bool; one that replaces a model in a registry.

Exercise 7 — the ruled-out pattern. Describe a design that cannot use unique_ptr, and say what the fight is telling you.

Exercise 8 — cost. Say what returning a unique_ptr by value costs and which chapter's measurement establishes it.

16. Summary

  • std::move is the only transfer, so every hand-over is explicit and greppable.
  • A by-value unique_ptr parameter means you now own this — and consume(p) without the move does not compile.
  • Measured: the object was destroyed inside the callee, and the caller's pointer became 0x0 — guaranteed null, so testable.
  • Observers took nothing, and should never name a smart pointer.
  • Returning from a factory is free (guaranteed elision).
  • Measured: moving out of a vector leaves an empty slot with size() unchanged — taking and erasing are two operations.
  • Name transferring functions honestly.
  • unique_ptr rules out keeping and giving away — that shape means shared ownership or a wrong design.

The transfers are all in place and every listing in this module has created its objects with one function that has been used thirty times and explained never. There are two reasons to prefer it to writing new inside a smart pointer's constructor, and only one of them is the obvious tidiness argument. The other is a leak that the obvious form permits and this one does not — measured here as one object still alive after an exception, in a two-argument call where both arguments were allocated and the second constructor threw. The next chapter is that function, the attachment of a release action for a resource that is not memory at all, and a size measurement showing that the way you spell the release action changes what the smart pointer costs.

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.