Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 12 – Move semantics and smart pointers

std::unique_ptr: exclusive ownership

How std::unique_ptr owns one heap object and frees it automatically, how ownership moves between functions, and how deleters wrap C resources.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Each object has one owner, so a tree frees itself · Ownership changes hands only by moving · The object is freed on every path, exceptions included · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Manage heap objects with std::unique_ptr and std::make_unique
  • Pass, return and borrow objects that a std::unique_ptr owns
  • Wrap C resources such as std::FILE* with custom deleters
  • Pass C out-parameters with std::out_ptr

Before you start

A function that calls new and then throws before its delete leaks: in this lesson’s run, with_new() left one Buffer behind, and the same function written with std::make_unique left none. std::unique_ptr<T>, from <memory>, owns one object on the heap and deletes it when the unique_ptr is destroyed, reset or given another object. exclusive ownership: The ownership model of std::unique_ptr: exactly one owner holds an object at any time and deletes it. The owner cannot be copied, only moved, and the owner that was moved from becomes null. means that exactly one unique_ptr owns the object at any time: its copy constructor is deleted, so ownership changes hands only by moving, and the unique_ptr that was moved from becomes null. You create one with std::make_unique<T>(args...), which forwards its arguments to T’s constructor. With the default deleter, both standard libraries make it exactly the size of a raw pointer. It replaces almost every new and delete in modern C++, because the delete then happens on every path out of a scope, exceptions included.

Each object has one owner, so a tree frees itself

Each Node owns its children through a std::vector<std::unique_ptr<Node>>, so the program builds a tree of five nodes and never writes delete:

tree.cpp C++ · tree.cpp
// Each Node owns its children through std::unique_ptr, so destroying the root destroys the whole tree without a
// single delete in the program.
#include <memory>
#include <print>
#include <string>
#include <utility>
#include <vector>

struct Node {
    std::string name;
    std::vector<std::unique_ptr<Node>> children;

    explicit Node(std::string n) : name(std::move(n)) {}
    ~Node() { std::println("    freed {}", name); }

    Node* add(std::string child) {
        children.push_back(std::make_unique<Node>(std::move(child)));
        return children.back().get();  // the caller may use the child, but does not own it
    }
};

int main() {
    std::unique_ptr<Node> root = std::make_unique<Node>("src");
    Node* lib = root->add("lib");
    lib->add("lexer.cpp");
    lib->add("parser.cpp");
    root->add("main.cpp");
    std::println("sizeof(std::unique_ptr<Node>) = {}, sizeof(Node*) = {}", sizeof(root), sizeof(lib));
    std::println("root.reset():");
    root.reset();
    std::println("root is empty: {}", root == nullptr);
}
Five nodes freed by one reset(), on both libraries Python · tree_session.py
from session import run

# Built with AddressSanitizer too, which would report a node freed twice or read after it was freed.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address tree.cpp -o tree && ./tree
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address tree.cpp -o tree-gcc && ./tree-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address tree.cpp -o tree && ./tree
sizeof(std::unique_ptr<Node>) = 8, sizeof(Node*) = 8
root.reset():
    freed src
    freed main.cpp
    freed lib
    freed parser.cpp
    freed lexer.cpp
root is empty: true
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address tree.cpp -o tree-gcc && ./tree-gcc
sizeof(std::unique_ptr<Node>) = 8, sizeof(Node*) = 8
root.reset():
    freed src
    freed lib
    freed lexer.cpp
    freed parser.cpp
    freed main.cpp
root is empty: true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tree_session.py

The diagram shows who owns whom once the tree is built:

A unique_ptr named root owns node src, which owns lib and main.cpp; lib owns two nodes. A raw pointer only observes lib.Every node has exactly one ownerrooton the stackNode "src"Node "lib"Node "main.cpp""lexer.cpp""parser.cpp"Node* libon the stackowns, through a std::unique_ptronly observes, through a raw pointerDestroying root frees all five nodes.

Owning pointers form a tree; a raw pointer only observes

Text description of the diagram

The diagram is titled "Every node has exactly one owner".

  1. At the top, root is a std::unique_ptr on the stack. A solid arrow, meaning "owns, through a std::unique_ptr", leads from it to the node "src" on the heap.
  2. The node "src" owns two children, the nodes "lib" and "main.cpp", each through a solid arrow.
  3. The node "lib" owns two children, "lexer.cpp" and "parser.cpp".
  4. At the bottom right, Node* lib is a raw pointer on the stack. A dashed arrow, meaning "only observes, through a raw pointer", leads from it to the node "lib".

A note at the bottom says that destroying root frees all five nodes. The raw pointer frees nothing.

root.reset() deletes the root node, whose destructor prints its name and then destroys its members, the vector of children, which deletes each child the same way. The unique_ptr is 8 bytes, like a Node*, and AddressSanitizer reports nothing. The two runs free the children in opposite orders, and both are right: the standard says that a vector destroys all its elements, not in which order, and libc++ starts from the back while libstdc++ starts from the front. Code that depends on that order needs an explicit loop. Step through the build and the reset, in libstdc++’s order:

Interactive · step by step A tree owned through unique_ptr, then reset Step through main of tree.cpp. Solid arrows are unique_ptrs that own a node; the dashed arrow is a raw pointer.
A tree owned through unique_ptr, then reset, step 8 of 8. Showing root (empty), Node* lib (dangles: lib was freed). In focus: Node* lib.rootemptyNode* libdangles: lib was freed

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. make_unique<Node>("src"). std::make_unique creates the Node src on the heap and returns the unique_ptr that owns it, root, a local of main.
  2. root->add("lib"). add() pushes a unique_ptr to a new Node into src's vector of children, so src owns lib. It returns children.back().get(), a plain Node* that only observes.
  3. Three more nodes. Two calls of lib->add give lib the children lexer.cpp and parser.cpp, and root->add("main.cpp") gives src a second child. Every node has exactly one owner.
  4. 8 bytes. sizeof(std::unique_ptr<Node>) and sizeof(Node*) both printed 8: with the default deleter, the owner takes no more space than a raw pointer.
  5. root.reset(). reset() deletes src. src's destructor prints freed src first, and only then are its members destroyed, the vector of children among them.
  6. lib and its children. With libstdc++ the vector destroys its elements from the front: lib prints freed lib, and its own vector then frees lexer.cpp and parser.cpp.
  7. main.cpp, then src's memory. Then main.cpp is freed and the memory of src is released. libc++ freed the same five nodes from the back of each vector instead: main.cpp before lib, parser.cpp before lexer.cpp.
  8. A pointer that dangles. root is empty, as the run printed, and AddressSanitizer found nothing. The Node* lib still holds the old address: an observing pointer must not be used once its owner is gone, and never deleted.

add() returns children.back().get(), a plain Node*. get() gives access without giving up ownership, so lib may be used to add grandchildren, but it must never be deleted, and it dangles once the tree is gone. Rule R.3 of the C++ Core Guidelines says that a raw pointer never owns, and rule R.20 that ownership is expressed with a smart pointer.

Key idea

Exactly one unique_ptr owns each object, and raw pointers and references only observe it: that is the normal shape of code built on unique_ptr.

Checkpoint

Check: One owner per object

  1. Question 1 of 2 How many nodes did root.reset() free in tree.cpp, with either library?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 5 nodes

    reset() deleted src, whose vector of children deleted lib and main.cpp, and lib's vector deleted lexer.cpp and parser.cpp: five freed lines on both libraries, in two different orders.

  2. Question 2 of 2 After root.reset(), what may the code still do with Node* lib?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing that reads through it; it dangles, and it must never be deleted

    Observing pointers are fine while the owner lives. Once the owner frees the object, they dangle.

Ownership changes hands only by moving

One report is handed out of a function, lent to another and handed over to a third:

ownership.cpp C++ · ownership.cpp
// A std::unique_ptr can be moved but not copied, so ownership moves only where the code says std::move.
// Built with -DCOPY, the program also tries to copy one.
#include <memory>
#include <print>
#include <string>
#include <utility>

struct Report {
    std::string title;
    explicit Report(std::string t) : title(std::move(t)) { std::println("    built {}", title); }
    ~Report() { std::println("    freed {}", title); }
};

std::unique_ptr<Report> make_report(std::string title) {
    return std::make_unique<Report>(std::move(title));  // ownership leaves through the return value
}

void archive(std::unique_ptr<Report> report) {  // a by-value unique_ptr parameter takes ownership
    std::println("    archiving {}", report->title);
}                                                // and the report is freed when it goes out of scope here

void print(const Report& report) {               // a reference only borrows
    std::println("    printing {}", report.title);
}

int main() {
    std::println("auto q3 = make_report(\"Q3 sales\");");
    auto q3 = make_report("Q3 sales");
    std::println("print(*q3);");
    print(*q3);
#ifdef COPY
    std::unique_ptr<Report> copy = q3;
#endif
    std::println("archive(std::move(q3));");
    archive(std::move(q3));
    std::println("q3 is empty now: {}", q3 == nullptr);
    std::println("q3 = std::make_unique<Report>(\"Q4 plan\"); then the end of main:");
    q3 = std::make_unique<Report>("Q4 plan");
}
A copy that does not compile, then three hand-overs Python · ownership_session.py
from session import run

# First the copy (-DCOPY), keeping only the error lines about ownership.cpp; then the program as written.
run("""
clang++ -std=c++23 -Wall -Wextra -DCOPY -fsyntax-only ownership.cpp 2>&1 | grep '^ownership.cpp'
g++-16 -std=c++23 -Wall -Wextra -DCOPY -fsyntax-only ownership.cpp 2>&1 | grep '^ownership.cpp'
clang++ -std=c++23 -Wall -Wextra -fsanitize=address ownership.cpp -o ownership && ./ownership
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address ownership.cpp -o ownership-gcc
./ownership > clang.txt && ./ownership-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -DCOPY -fsyntax-only ownership.cpp 2>&1 | grep '^ownership.cpp'
ownership.cpp:32:29: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<Report>'
$ g++-16 -std=c++23 -Wall -Wextra -DCOPY -fsyntax-only ownership.cpp 2>&1 | grep '^ownership.cpp'
ownership.cpp: In function 'int main()':
ownership.cpp:32:36: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Report; _Dp = std::default_delete<Report>]'
ownership.cpp:32:36: note: use '-fdiagnostics-all-candidates' to display considered candidates
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address ownership.cpp -o ownership && ./ownership
auto q3 = make_report("Q3 sales");
    built Q3 sales
print(*q3);
    printing Q3 sales
archive(std::move(q3));
    archiving Q3 sales
    freed Q3 sales
q3 is empty now: true
q3 = std::make_unique<Report>("Q4 plan"); then the end of main:
    built Q4 plan
    freed Q4 plan
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address ownership.cpp -o ownership-gcc
$ ./ownership > clang.txt && ./ownership-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
GCC prints the same

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ownership_session.py

Copying a unique_ptr does not compile, on either compiler, because its copy constructor is deleted. Everything else follows from that one rule:

  • Return it by value to hand ownership to the caller, as make_report does; the returned local is moved, or built in place, never copied.
  • Take it by value to take ownership, as archive does. The caller must write std::move(q3), which makes the hand-over visible, and the report is freed when archive returns. Rule R.32 of the Core Guidelines recommends this signature for a function that keeps or destroys what it is given.
  • Take a reference or a raw pointer to borrow, as print does: such a function uses the object and says nothing about its lifetime.
  • Assign a new object to a unique_ptr that already owns one, and the old object is deleted; reset() does the same with a raw pointer, or with no argument to leave the unique_ptr empty.

Step through main and watch who owns the report:

Interactive · message by message One report, handed out, lent and handed over Step through main of ownership.cpp. Each sub-label says what the function owns at that moment.
One report, handed out, lent and handed over, step 7 of 7. Messages so far: main to make_report: make_report("Q3 sales"); make_report to main: std::unique_ptr<Report>; main to archive: std::move(q3).mainq3 owns Q4 planmake_reportowns nothingarchiveowns nothingmake_report("Q3 sales")std::unique_ptr<Report>std::move(q3)

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. make_report("Q3 sales"). main calls make_report, which creates the Report with std::make_unique: the run printed built Q3 sales.
  2. Returned by value. make_report returns its unique_ptr by value. The result is moved or built in place, never copied, so q3 in main is now the only owner.
  3. print(*q3). print takes a const Report&, so it only borrows the report: the run printed printing Q3 sales, and q3 still owns it afterwards.
  4. A copy does not compile. Built with -DCOPY, std::unique_ptr<Report> copy = q3; is rejected by both compilers: the copy constructor of unique_ptr is deleted, so there can never be a second owner.
  5. archive(std::move(q3)). archive takes a unique_ptr by value. std::move(q3) makes the hand-over visible: archive's parameter is moved from q3, and q3 becomes null.
  6. Freed when archive returns. archive prints archiving Q3 sales and returns. Its parameter is destroyed with it, and the run printed freed Q3 sales before main went on.
  7. A new object. q3 is empty now, as the run printed. Assigning std::make_unique<Report>("Q4 plan") gives it a new report, which the end of main frees: built Q4 plan, freed Q4 plan.

Checkpoint

Check: Handing ownership in and out of functions

  1. Question 1 of 2 void archive(std::unique_ptr<Report> report); How does main hand it the report that q3 owns?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: archive(std::move(q3));

    A by-value unique_ptr parameter takes ownership, and the caller's std::move shows where it happens.

  2. Question 2 of 2 A function only reads a report during the call. Which parameter type says so?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: const Report&

    Pass the object, not the owner, to code that only uses it: a reference, or a raw pointer when no object is a valid argument.

The object is freed on every path, exceptions included

An exception that leaves a function between new and delete skips the delete. The same function written with std::make_unique frees the object while the exception passes through, because the unique_ptr is a local whose destructor runs during stack unwinding: What happens while a thrown exception travels to its catch block: every local object constructed since the try block began, and not yet destroyed, is destroyed in reverse order before the handler runs. More in “Destructors and RAII in C++”:

leak.cpp C++ · leak.cpp
// An exception between new and delete leaks; the same function with std::make_unique does not.
#include <memory>
#include <print>
#include <stdexcept>

struct Buffer {
    static inline int alive = 0;
    Buffer() { ++alive; }
    ~Buffer() { --alive; }
};

void parse(const Buffer&) { throw std::runtime_error("bad input"); }

void with_new() {
    Buffer* buffer = new Buffer;
    parse(*buffer);
    delete buffer;  // never reached
}

void with_unique_ptr() {
    auto buffer = std::make_unique<Buffer>();
    parse(*buffer);
}  // the unique_ptr's destructor runs while the exception passes through

int main() {
    int before = Buffer::alive;
    try { with_new(); } catch (const std::exception&) {}
    std::println("Buffers leaked by with_new():        {}", Buffer::alive - before);

    before = Buffer::alive;
    try { with_unique_ptr(); } catch (const std::exception&) {}
    std::println("Buffers leaked by with_unique_ptr(): {}", Buffer::alive - before);
}
Objects left behind by an exception Python · leak_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra leak.cpp -o leak && ./leak
g++-16 -std=c++23 -Wall -Wextra leak.cpp -o leak-gcc && ./leak-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra leak.cpp -o leak && ./leak
Buffers leaked by with_new():        1
Buffers leaked by with_unique_ptr(): 0
$ g++-16 -std=c++23 -Wall -Wextra leak.cpp -o leak-gcc && ./leak-gcc
Buffers leaked by with_new():        1
Buffers leaked by with_unique_ptr(): 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 leak_session.py

Step through both calls as the exception leaves them:

Interactive · message by message One exception, two functions Step through with_new() and then with_unique_ptr(), each time until the exception reaches the catch in main.
One exception, two functions, step 6 of 6. Messages so far: main to the function: with_new(); the function to parse: parse(*buffer); parse to main: std::runtime_error; main to the function: with_unique_ptr(); the function to parse: parse(*buffer); parse to main: std::runtime_error.maincatches the exceptionthe functionfreed while unwindingparsethrows: bad inputwith_new()parse(*buffer)std::runtime_errorwith_unique_ptr()parse(*buffer)std::runtime_error

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. with_new(). main calls with_new(), which creates a Buffer with new and keeps its address in a plain Buffer*.
  2. parse throws. with_new passes the Buffer to parse, which throws std::runtime_error with the text bad input.
  3. The delete is skipped. The exception leaves parse and with_new on its way to the catch in main. Stack unwinding destroys the local Buffer*, which frees nothing, so delete buffer; never runs: 1 Buffer leaked.
  4. with_unique_ptr(). Now main calls with_unique_ptr(), which creates the Buffer with std::make_unique and keeps it in a local unique_ptr.
  5. parse throws again. parse throws the same exception as before.
  6. Freed during unwinding. This time stack unwinding destroys a unique_ptr, and its destructor deletes the Buffer while the exception passes through: 0 leaked, as the run printed on both compilers.

The version with new leaked one Buffer; the version with std::make_unique leaked none. Destroying a raw pointer frees nothing, while destroying a unique_ptr deletes what it owns. Each failed call of with_new() leaks one more Buffer, while with_unique_ptr() leaks none however often parse fails. Rule R.23 of the Core Guidelines asks for make_unique instead of a new handed to a unique_ptr, which also keeps the type from being written twice.

Checkpoint

Check: Freeing on every path

  1. Question 1 of 2 How many Buffers did with_new() leak when parse threw, on both compilers?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 1 buffers

    The exception left with_new before delete buffer; ran, so one Buffer was left behind. with_unique_ptr() leaked none.

  2. Question 2 of 2 Why did with_unique_ptr() leak nothing, although its function body never reached its end?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Stack unwinding destroyed its local unique_ptr, whose destructor deleted the Buffer

    The delete moved from a statement that can be skipped into a destructor that always runs.

Arrays, C resources and out-parameters have their own tools

A few more tools come with unique_ptr, each for one specific situation:

  • std::unique_ptr<T[]> owns an array made with new T[n] and calls delete[]; a std::vector is usually better.
  • A custom deleter: The second template argument of std::unique_ptr: an object whose call operator releases what the unique_ptr holds instead of delete, such as one that closes a std::FILE* with std::fclose. is a second template argument whose call operator frees the resource, such as std::fclose for a std::FILE*. The unique_ptr calls it instead of delete, so a C resource goes back through its own library’s function.
  • std::out_ptr (C++23) lets a C function that returns a new object through a T** parameter fill a unique_ptr.
  • release() gives up ownership without freeing anything and returns the raw pointer, which someone else must then free.
  • The pimpl idiom: Pointer to implementation: a class keeps its private data in a separate type that its header only declares and reaches it through a pointer, usually a std::unique_ptr, so the data can change without changing the header. hides a class’s data behind a unique_ptr to a type that is only declared in the header.

Step through the six questions that choose among them:

Interactive · stage by stage Which unique_ptr tool for which resource Step through six kinds of resource. Each stage asks one question and names the tool that answers it.
Which unique_ptr tool for which resource, step 6 of 6. Showing One object from new?, An array of n objects?, A C resource, a FILE*?, Returned through a T**?, Giving ownership up?, Hiding a class's data?. In focus: Hiding a class's data?.One object from new?std::make_unique<T>(args)An array of n objects?a vector, or unique_ptr<T[]>A C resource, a FILE*?a custom deleterReturned through a T**?std::out_ptr, from C++23Giving ownership up?someone else must free itHiding a class's data?pimpl: unique_ptr<Impl>

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One object. For one object on the heap, std::make_unique<T>(args...) creates it and the unique_ptr that owns it, with the type written once, as rule R.23 asks.
  2. An array. std::unique_ptr<T[]> owns an array made with new T[n] and frees it with delete[]. A std::vector, which also keeps the size, is usually the better owner.
  3. A C resource. A custom deleter, the second template argument, is called instead of delete: a std::FILE* goes back through std::fclose.
  4. An out-parameter. Some C functions create an object and return it through a T** parameter. std::out_ptr, new in C++23, lets such a function fill a unique_ptr.
  5. Giving ownership up. release() gives up ownership without freeing anything and returns the raw pointer, which someone else must then free. get() also returns the pointer, but keeps ownership.
  6. Hiding the data. The pimpl idiom keeps a class's data in a type that the header only declares, behind a std::unique_ptr to it.

Checkpoint

Check: Arrays, deleters, out_ptr, release() and pimpl

  1. Question 1 of 2 Node* p = root.release(); What does root own afterwards, and who frees the node?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing; the code that holds p must free the node, or hand it to a new owner

    get() lends, release() lets go, reset() frees. Only release() leaves an object that nobody owns.

  2. Question 2 of 2 A C library offers int sensor_open(int id, sensor** out). Which tool lets the call fill a unique_ptr directly?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::out_ptr, from C++23

    A C function that returns a new object through a pointer to a pointer is what std::out_ptr exists for.

Practise: owners, borrowers and the tools

Questions on the four ideas above; the one you know least comes first, and a missed question returns later.

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 How many bytes did sizeof(std::unique_ptr<Node>) print on the 64-bit Mac in this lesson, with both libraries?

    Type a number.

    Show the answer: Question 1 of 13

    Answer: 8 bytes

    With the default deleter, both libraries store only the pointer, so a unique_ptr is the size of a Node*, 8 bytes on a 64-bit platform.

  2. Question 2 of 13 In the tree example, the two libraries freed the children of a node in opposite orders. Why is that allowed?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: The standard requires a vector to destroy its elements but does not say in which order

    The container requirements say only that the destructor is applied to every element. libc++ destroys from the back and libstdc++ from the front; code that needs an order has to clear the vector in an explicit loop.

  3. Question 3 of 13 A helper receives Node* lib from add() and runs delete lib;. What happens when the tree is later reset?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: The unique_ptr in src's vector deletes the node a second time, which is undefined behaviour

    Rule R.3 of the Core Guidelines: a raw pointer never owns, so it is never deleted.

  4. Question 4 of 13 What happens with this code?

    Read the code, then choose one answer.

    auto a = std::make_unique<Report>("Q3");
    auto b = a;
    Show the answer: Question 4 of 13

    Answer: It does not compile, because std::unique_ptr's copy constructor is deleted

    Exclusive ownership is enforced by the type: a unique_ptr cannot be copied. auto b = std::move(a); would compile, leaving a empty and b the owner.

  5. Question 5 of 13 A function stores the Report it is given and keeps it. Which parameter type says that?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: std::unique_ptr<Report>, taken by value

    A unique_ptr taken by value receives the ownership: the caller has to write std::move, and the function decides what happens to the object. A reference or a raw pointer only borrows it.

  6. Question 6 of 13 q3 owns the report Q3 sales. What does q3 = std::make_unique<Report>("Q4 plan"); do?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: Builds Q4 plan, then deletes Q3 sales, and q3 owns the new report

    reset() does the same with a raw pointer, or with no argument to leave the unique_ptr empty.

  7. Question 7 of 13 std::unique_ptr<Report> make() { auto r = std::make_unique<Report>("Q1"); return r; } Does the return need std::move(r)?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: No, because a returned local is moved, or built in place, without help

    return std::move(r); would compile but only adds a cast, and with Clang and GCC it draws a warning about a pessimizing move.

  8. Question 8 of 13 auto b = std::make_unique<Buffer>(); parse(*b); What happens to the Buffer if parse throws?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: It is deleted while the exception leaves the function, because b's destructor runs during unwinding

    Local objects are destroyed during stack unwinding, so the unique_ptr deletes the Buffer whatever path leaves the scope. The lesson's run counted 0 leaked buffers this way and 1 with new and delete.

  9. Question 9 of 13 Why does rule R.23 prefer auto b = std::make_unique<Buffer>(); to std::unique_ptr<Buffer> b(new Buffer);?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: It writes the type once and leaves no new in the code to pair with a delete

    make_unique forwards its arguments to the constructor, so creating and owning are one step.

  10. Question 10 of 13 A function creates auto a = std::make_unique<Buffer>(); and then Buffer* b = new Buffer;, and the next call throws. How many Buffers leak?

    Type a number.

    Show the answer: Question 10 of 13

    Answer: 1 buffers

    Unwinding destroys both locals: a deletes its Buffer, while destroying the raw pointer b frees nothing, so the Buffer made with new is left behind.

  11. Question 11 of 13 An array is made with new Sensor[3]. Which owner frees it the right way?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: std::unique_ptr<Sensor[]>, which calls delete[]

    std::unique_ptr<T[]> exists for arrays from new T[n]; a std::vector<Sensor> is usually the better owner still.

  12. Question 12 of 13 What is a custom deleter of a std::unique_ptr?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: Its second template argument, whose call operator frees the resource instead of delete

    The deleter is part of the unique_ptr's type, so the right release function is chosen where the type is written.

  13. Question 13 of 13 A class's header should stay the same when its private members change. Which tool from the lesson fits?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: The pimpl idiom, with the data in a type the header only declares, behind a std::unique_ptr

    pimpl trades a pointer and an allocation for a header that does not change with the class's private data.

Custom deleters for C resources, with sizes measured Pro: opens with a Pro, Premium or Ultimate pass

C libraries hand out resources that must go back through their own functions, and a unique_ptr with the right deleter returns them on every path. This part compares five kinds of deleter by what they cost in bytes, and fills a unique_ptr straight from a C out-parameter.

In this part:

  • A FILE* that closes itself on every path
  • What a deleter costs, in bytes
  • Filling a unique_ptr from a C out-parameter

6 runnable examples (C++, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Ownership bugs reproduced: two owners, deep lists and pimpl Pro: opens with a Pro, Premium or Ultimate pass

unique_ptr removes most memory bugs, and the ones that remain are about who owns what and when ownership ends. Each one below is reproduced or measured on both compilers, then fixed.

In this part:

  • Two owners, arrays and release(), caught by AddressSanitizer
  • Destroying a long list: the stack it needs, measured
  • pimpl and the incomplete type
  • Your turn: a binary search tree of unique_ptr nodes

8 runnable examples (C header, C++, C++ (widget.cpp), C++ (widget_main.cpp), Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    Why can a std::unique_ptr be moved but not copied?

    Because it models exclusive ownership: exactly one unique_ptr owns the object and deletes it. A copy would create a second owner, and both would delete the object, so the copy constructor and the copy assignment operator are deleted. A move transfers the ownership instead: the destination takes the pointer and the source becomes null, so there is still one owner and one delete. That is also why a class with a unique_ptr member cannot be copied unless it defines what a copy means, but can be moved without any code. To hand an object to a function, pass the unique_ptr by value with std::move; to lend it, pass a reference or the raw pointer from get().

  2. Mid to senior

    A message queue is a singly linked list whose nodes own the next node through std::unique_ptr. With about a million queued messages the service crashes at shutdown, but only in the debug build, and on a worker thread it crashes with far fewer messages. Explain it, estimate the limits, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A C library offers int db_open(const char* path, db** out) and void db_close(db*). Design the C++ type that owns a db handle: the deleter, how it is created from db_open, how errors are reported, and what it costs in size. Would you use std::unique_ptr<db, decltype(&db_close)>?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A class uses std::unique_ptr<Impl> for the pimpl idiom, with Impl only declared in the header. Every file that creates or destroys the class fails to compile with "invalid application of sizeof to an incomplete type". Explain the error, fix it, and list every member function that must be defined in the source file.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • std::unique_ptr<T> owns one heap object and deletes it when it is destroyed, reset or reassigned; with the default deleter it is the size of a raw pointer.
  • It cannot be copied, only moved, so std::move marks every place where ownership changes hands.
  • Return a unique_ptr by value to hand ownership out, take it by value to take ownership, and pass a reference or a raw pointer to lend the object.
  • Create objects with std::make_unique: the object is freed on every path out of the scope, exceptions included.
  • get() and release() both return the raw pointer; only release() gives up ownership, and someone else must free the object.

Terms in this lesson

custom deleter
The second template argument of std::unique_ptr: an object whose call operator releases what the unique_ptr holds instead of delete, such as one that closes a std::FILE* with std::fclose.
exclusive ownership
The ownership model of std::unique_ptr: exactly one owner holds an object at any time and deletes it. The owner cannot be copied, only moved, and the owner that was moved from becomes null.
pimpl idiom
Pointer to implementation: a class keeps its private data in a separate type that its header only declares and reaches it through a pointer, usually a std::unique_ptr, so the data can change without changing the header.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.