AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

07 Memory, ownership and RAII / Lesson 4 of 6

std::unique_ptr — exclusive ownership, enforced by the type

15 min · +10 XP · 2 exercises · 7 questions

Read the text15 min
Exercises0/2
Quiz7 q

Quick answer

What is unique_ptr in C++?

std::unique_ptr<T> is a C++ smart pointer that exclusively owns one heap object and deletes it in its destructor, when the pointer goes out of scope. It cannot be copied, only moved with std::move, which transfers ownership and leaves the source null, so a second owner is a compile error. It costs the same as a raw pointer; create one with std::make_unique<T>(args).

std::unique_ptr<T> is the IntBuffer of lesson 2 written once, for every type: a small object that holds a pointer, deletes it in its destructor, and cannot be copied. That last property is the point. A raw owning pointer relies on a rule that one owner deletes; a unique_ptr makes a second owner a compile error. It costs nothing over the raw pointer — one word of storage with the default deleter, the same indirection — so it is the default way to hold a heap object in modern C++, and the new-and-remember-to-delete of lesson 2 becomes std::make_unique.

Creating and using

#include <memory>

auto p = std::make_unique<std::string>("hello");   // std::unique_ptr<std::string>
std::cout << *p << ' ' << p->size() << '\n';       // hello 5
if (p) std::cout << "owns something\n";            // converts to bool: non-null

std::make_unique<T>(args…) performs new T(args…) and wraps the result. Prefer it over std::unique_ptr<T>(new T(…)): it never names new, it cannot leak when it appears as one of several arguments to a function (an evaluation-order hazard before C++17), and std::make_unique<T[]>(n) value-initialises an array. *p and p-> work as for a raw pointer; p in a condition is true when it holds an object.

When p goes out of scope the string is deleted. There is nothing else to write.

Move-only

auto a = std::make_unique<int>(1);
auto b = a;               // error: the copy constructor is deleted
auto c = std::move(a);    // ownership transfers; a is now null
std::cout << (a ? "a owns" : "a is empty") << '\n';   // a is empty

The copy operations are deleted, so the only way to hand the object to another unique_ptr is std::move, which leaves the source holding nullptr — a defined, testable state, not a valid-but-unspecified one. Module 9 explains rvalue references and what std::move does in general; for unique_ptr the contract is simple: after std::move(a), a is null and c owns.

The three escape hatches

CallEffectOwnership afterwards
p.get()returns the raw pointerstill p's; the raw pointer is a borrow
p.release()returns the raw pointer and sets p to nullyours — you must delete it or hand it to another owner
p.reset() / p.reset(q)deletes the current object, then holds q (or nothing)p owns q

get() is for calling code that takes a T* and does not keep it. release() is rare — passing to a C API that takes ownership, or re-wrapping in a different smart pointer — and a release() whose result is dropped is a leak. reset() is how you destroy early, at a point you choose rather than at the closing brace.

Passing and returning

The parameter type is the ownership statement:

void inspect(const Document& d);              // lends: the caller keeps owning
void annotate(Document& d);                   // lends, may modify
void maybe_use(Document* d);                  // lends, may be null
void archive(std::unique_ptr<Document> d);    // takes ownership: call with std::move
void reseat(std::unique_ptr<Document>& d);    // may replace what the caller owns (rare)

std::unique_ptr<Document> open(const std::string& path);   // factory: the caller owns the result

Call a sink with archive(std::move(doc)); afterwards doc is null, and the parameter inside archive dies at its closing brace, taking the document with it. Lend with inspect(*doc) or maybe_use(doc.get()). Return from a factory by value — return doc; moves automatically, and C++17 guarantees no copy — and the caller writes auto doc = open("a.txt");. Passing const std::unique_ptr<T>& to a function that merely reads the object is a common mistake: it forces the caller to own a unique_ptr, where const T& would accept any object at all.

Arrays and custom deleters

auto buf = std::make_unique<int[]>(n);        // n ints, zero-initialised; delete[] in the destructor
buf[3] = 7;                                   // operator[], no bounds check

std::unique_ptr<FILE, decltype(&std::fclose)> file(std::fopen("x.txt", "r"), &std::fclose);

The T[] specialisation calls delete[] and provides [] — the right tool when the size is fixed at creation and a std::vector would be overkill, which is rarer than it sounds; std::vector is usually the answer. A second template argument names a deleter: any callable invoked instead of delete, which is how a unique_ptr closes a FILE*, frees a C library handle, or returns a connection to a pool. A stateless lambda or function object as the deleter keeps the pointer one word wide; a function pointer adds a second.

Linked structures

struct Node {
    int value;
    std::unique_ptr<Node> next;
    Node(int v, std::unique_ptr<Node> n) : value(v), next(std::move(n)) {}
};

std::unique_ptr<Node> head;
head = std::make_unique<Node>(1, std::move(head));   // push front: the new node owns the old head

std::unique_ptr<Node> old = std::move(head);         // pop front: detach the head …
head = std::move(old->next);                         // … take its successor …
                                                     // … and old deletes the node at the brace

Each node owns the next, so the chain is freed by whichever unique_ptr holds the head. The pop is written in two steps on purpose: the tempting one-liner head = std::move(head->next) deletes the old node inside the assignment while still referring to a member of it, and the standard's wording for that assignment leaves the result undefined. Detach, then take.

Destroying a long chain is recursive — head's destructor deletes the first node, whose next deletes the second, one destructor frame per node — and overflows the stack for a list of a few hundred thousand. The fix is a destructor that pops from the front in a loop until the chain is empty; the second exercise writes it.

In containers and hierarchies

std::vector<std::unique_ptr<Shape>> is the standard way to own a collection of polymorphic objects (Module 10): shapes.push_back(std::make_unique<Circle>(2.0));, and the vector's destructor deletes every shape. A unique_ptr member makes a class own a heap object without writing a destructor at all — the rule of zero, Module 9 lesson 4.

Pitfalls

MistakeWhat happens
std::unique_ptr<T> a(raw); std::unique_ptr<T> b(raw);Two owners of one object: double delete.
Keeping p.get() after p is destroyed or resetDangling raw pointer.
p.release(); with the result discardedLeak — nobody deletes.
f(std::unique_ptr<T>(new T), g()) in C++14If g() throws between the new and the wrap, a leak; make_unique closes the gap.
Using a after std::move(a) as if it still ownedIt is null; *a is undefined behaviour.
std::unique_ptr<T> p = new T;Does not compile: the constructor is explicit. Write make_unique.

Key takeaways

  • std::unique_ptr<T> owns one heap object, deletes it in its destructor, and costs the same as a raw pointer.
  • Create with std::make_unique; copying is a compile error; std::move transfers and leaves the source null.
  • get() borrows, release() gives up ownership to you, reset() deletes now.
  • By value = give (call with std::move); T&, const T& or T* = lend; factories return by value.
  • T[] and custom deleters cover arrays and non-memory resources; a unique_ptr chain needs a two-step pop and an iterative destructor.

Common questions

Why can't you copy a unique_ptr?

Its copy constructor and copy assignment are deleted, because a copy would create two owners that both delete the same object. Ownership is transferred instead with std::move(p), after which p is guaranteed to be null and can be tested.

What is the difference between get(), release() and reset()?

get() returns the raw pointer while the unique_ptr keeps ownership — a borrow. release() returns the raw pointer and sets the unique_ptr to null, so you must delete it or hand it to another owner. reset() deletes the current object now, optionally taking a new one.

Why use make_unique instead of new?

std::make_unique<T>(args) never names new, so there is no raw pointer to leak or to wrap twice. Before C++17 it also closed a leak where f(std::unique_ptr<T>(new T), g()) could allocate and then have g() throw before the pointer was wrapped. make_unique<T[]>(n) value-initialises an array.

How do you pass a unique_ptr to a function?

The parameter type states the ownership. Take std::unique_ptr<T> by value to take ownership, called with std::move(p); take const T&, T& or T* just to use the object. Passing const std::unique_ptr<T>& needlessly forces callers to own a unique_ptr. Factories return std::unique_ptr<T> by value.

Should I use unique_ptr or shared_ptr?

Use std::unique_ptr by default: one owner and no overhead over a raw pointer. Use std::shared_ptr only when several owners with independent lifetimes genuinely exist, since every copy of it costs an atomic reference-count update.

Mark as readSign in to keep your place.

Smart Pointers in C++: Unique Pointer and Exclusive Ownership

A C++ unique pointer owns one heap object, deletes it automatically and can be moved but not copied. Creating one, passing ownership, and custom deleters.

  • Course: C++ study plan
  • Module: Memory, ownership and RAII
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: Clang 18 · C++20

What is unique_ptr in C++?

std::unique_ptr<T> is a C++ smart pointer that exclusively owns one heap object and deletes it in its destructor, when the pointer goes out of scope. It cannot be copied, only moved with std::move, which transfers ownership and leaves the source null, so a second owner is a compile error. It costs the same as a raw pointer; create one with std::make_unique<T>(args).

Lesson

std::unique_ptr<T> is the IntBuffer of lesson 2 written once, for every type: a small object that holds a pointer, deletes it in its destructor, and cannot be copied. That last property is the point. A raw owning pointer relies on a rule that one owner deletes; a unique_ptr makes a second owner a compile error. It costs nothing over the raw pointer — one word of storage with the default deleter, the same indirection — so it is the default way to hold a heap object in modern C++, and the new-and-remember-to-delete of lesson 2 becomes std::make_unique.

Creating and using

#include <memory>

auto p = std::make_unique<std::string>("hello");   // std::unique_ptr<std::string>
std::cout << *p << ' ' << p->size() << '\n';       // hello 5
if (p) std::cout << "owns something\n";            // converts to bool: non-null

std::make_unique<T>(args…) performs new T(args…) and wraps the result. Prefer it over std::unique_ptr<T>(new T(…)): it never names new, it cannot leak when it appears as one of several arguments to a function (an evaluation-order hazard before C++17), and std::make_unique<T[]>(n) value-initialises an array. *p and p-> work as for a raw pointer; p in a condition is true when it holds an object.

When p goes out of scope the string is deleted. There is nothing else to write.

Move-only

auto a = std::make_unique<int>(1);
auto b = a;               // error: the copy constructor is deleted
auto c = std::move(a);    // ownership transfers; a is now null
std::cout << (a ? "a owns" : "a is empty") << '\n';   // a is empty

The copy operations are deleted, so the only way to hand the object to another unique_ptr is std::move, which leaves the source holding nullptr — a defined, testable state, not a valid-but-unspecified one. Module 9 explains rvalue references and what std::move does in general; for unique_ptr the contract is simple: after std::move(a), a is null and c owns.

The three escape hatches

CallEffectOwnership afterwards
p.get()returns the raw pointerstill p's; the raw pointer is a borrow
p.release()returns the raw pointer and sets p to nullyours — you must delete it or hand it to another owner
p.reset() / p.reset(q)deletes the current object, then holds q (or nothing)p owns q

get() is for calling code that takes a T* and does not keep it. release() is rare — passing to a C API that takes ownership, or re-wrapping in a different smart pointer — and a release() whose result is dropped is a leak. reset() is how you destroy early, at a point you choose rather than at the closing brace.

Passing and returning

The parameter type is the ownership statement:

void inspect(const Document& d);              // lends: the caller keeps owning
void annotate(Document& d);                   // lends, may modify
void maybe_use(Document* d);                  // lends, may be null
void archive(std::unique_ptr<Document> d);    // takes ownership: call with std::move
void reseat(std::unique_ptr<Document>& d);    // may replace what the caller owns (rare)

std::unique_ptr<Document> open(const std::string& path);   // factory: the caller owns the result

Call a sink with archive(std::move(doc)); afterwards doc is null, and the parameter inside archive dies at its closing brace, taking the document with it. Lend with inspect(*doc) or maybe_use(doc.get()). Return from a factory by value — return doc; moves automatically, and C++17 guarantees no copy — and the caller writes auto doc = open("a.txt");. Passing const std::unique_ptr<T>& to a function that merely reads the object is a common mistake: it forces the caller to own a unique_ptr, where const T& would accept any object at all.

Arrays and custom deleters

auto buf = std::make_unique<int[]>(n);        // n ints, zero-initialised; delete[] in the destructor
buf[3] = 7;                                   // operator[], no bounds check

std::unique_ptr<FILE, decltype(&std::fclose)> file(std::fopen("x.txt", "r"), &std::fclose);

The T[] specialisation calls delete[] and provides [] — the right tool when the size is fixed at creation and a std::vector would be overkill, which is rarer than it sounds; std::vector is usually the answer. A second template argument names a deleter: any callable invoked instead of delete, which is how a unique_ptr closes a FILE*, frees a C library handle, or returns a connection to a pool. A stateless lambda or function object as the deleter keeps the pointer one word wide; a function pointer adds a second.

Linked structures

struct Node {
    int value;
    std::unique_ptr<Node> next;
    Node(int v, std::unique_ptr<Node> n) : value(v), next(std::move(n)) {}
};

std::unique_ptr<Node> head;
head = std::make_unique<Node>(1, std::move(head));   // push front: the new node owns the old head

std::unique_ptr<Node> old = std::move(head);         // pop front: detach the head …
head = std::move(old->next);                         // … take its successor …
                                                     // … and old deletes the node at the brace

Each node owns the next, so the chain is freed by whichever unique_ptr holds the head. The pop is written in two steps on purpose: the tempting one-liner head = std::move(head->next) deletes the old node inside the assignment while still referring to a member of it, and the standard's wording for that assignment leaves the result undefined. Detach, then take.

Destroying a long chain is recursive — head's destructor deletes the first node, whose next deletes the second, one destructor frame per node — and overflows the stack for a list of a few hundred thousand. The fix is a destructor that pops from the front in a loop until the chain is empty; the second exercise writes it.

In containers and hierarchies

std::vector<std::unique_ptr<Shape>> is the standard way to own a collection of polymorphic objects (Module 10): shapes.push_back(std::make_unique<Circle>(2.0));, and the vector's destructor deletes every shape. A unique_ptr member makes a class own a heap object without writing a destructor at all — the rule of zero, Module 9 lesson 4.

Pitfalls

MistakeWhat happens
std::unique_ptr<T> a(raw); std::unique_ptr<T> b(raw);Two owners of one object: double delete.
Keeping p.get() after p is destroyed or resetDangling raw pointer.
p.release(); with the result discardedLeak — nobody deletes.
f(std::unique_ptr<T>(new T), g()) in C++14If g() throws between the new and the wrap, a leak; make_unique closes the gap.
Using a after std::move(a) as if it still ownedIt is null; *a is undefined behaviour.
std::unique_ptr<T> p = new T;Does not compile: the constructor is explicit. Write make_unique.

Key takeaways

  • std::unique_ptr<T> owns one heap object, deletes it in its destructor, and costs the same as a raw pointer.
  • Create with std::make_unique; copying is a compile error; std::move transfers and leaves the source null.
  • get() borrows, release() gives up ownership to you, reset() deletes now.
  • By value = give (call with std::move); T&, const T& or T* = lend; factories return by value.
  • T[] and custom deleters cover arrays and non-memory resources; a unique_ptr chain needs a two-step pop and an iterative destructor.

Common questions

Why can't you copy a unique_ptr?

Its copy constructor and copy assignment are deleted, because a copy would create two owners that both delete the same object. Ownership is transferred instead with std::move(p), after which p is guaranteed to be null and can be tested.

What is the difference between get(), release() and reset()?

get() returns the raw pointer while the unique_ptr keeps ownership — a borrow. release() returns the raw pointer and sets the unique_ptr to null, so you must delete it or hand it to another owner. reset() deletes the current object now, optionally taking a new one.

Why use make_unique instead of new?

std::make_unique<T>(args) never names new, so there is no raw pointer to leak or to wrap twice. Before C++17 it also closed a leak where f(std::unique_ptr<T>(new T), g()) could allocate and then have g() throw before the pointer was wrapped. make_unique<T[]>(n) value-initialises an array.

How do you pass a unique_ptr to a function?

The parameter type states the ownership. Take std::unique_ptr<T> by value to take ownership, called with std::move(p); take const T&, T& or T* just to use the object. Passing const std::unique_ptr<T>& needlessly forces callers to own a unique_ptr. Factories return std::unique_ptr<T> by value.

Should I use unique_ptr or shared_ptr?

Use std::unique_ptr by default: one owner and no overhead over a raw pointer. Use std::shared_ptr only when several owners with independent lifetimes genuinely exist, since every copy of it costs an atomic reference-count update.

Exercises

Factory and sink

Practise the three ways a std::unique_ptr moves through a program: a factory that returns one, a lender that takes const Document&, and a sink that takes the pointer by value.

Document holds a title and a word count; its destructor prints destroyed <title>. Write:

  • std::unique_ptr<Document> make_document(const std::string& title, int words) — std::make_unique, print created <title>, return it;
  • void inspect(const Document& doc) — print <title> has <words> words;
  • void archive(std::unique_ptr<Document> doc) — print archived <title>; the document dies when the parameter does.

main reads n documents. For each: create it, inspect it (pass *doc), then if it has fewer than 100 words call doc.reset() and print discarded <title>, otherwise call archive(std::move(doc)). Then print owner empty: true or false (std::boolalpha) — after either path the owner in main is null. Finally print processed <n>.

Input: n, then n lines of <title> <words> (the title is one word). Output: five lines per document, then the count.

2
report 120
memo 40

prints

created report
report has 120 words
archived report
destroyed report
owner empty: true
created memo
memo has 40 words
destroyed memo
discarded memo
owner empty: true
processed 2

A stack that owns its nodes

Implement a stack as a chain of nodes where each node owns the next through a std::unique_ptr<Node>, so that no delete appears anywhere in the program.

Commands, one per line: push <v>, pop (print popped <v> or empty), peek (print top=<v> or empty), size (print size=<k>), print (print stack: <top> … <bottom> or stack: (empty)).

Push must be head_ = std::make_unique<Node>(v, std::move(head_)). Pop must be the two-step form from the lesson — detach the head into a local unique_ptr, then move its next into head_ — and the destructor must pop in a loop rather than let the chain destroy itself recursively.

Input: commands. Output: one line per command other than push.

push 1
push 2
push 3
print
pop
peek
size
pop
pop
pop
print

prints

stack: 3 2 1
popped 3
top=2
size=2
popped 2
popped 1
empty
stack: (empty)

In this module: Memory, ownership and RAII

← RAII — a resource's lifetime is an object's lifetime · std::shared_ptr and std::weak_ptr — counted ownership and its cycles →