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.
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
What you’ll learn · Before you start
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
On this page
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:
// 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);
} 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:
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".
- 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.
- The node "src" owns two children, the nodes "lib" and "main.cpp", each through a solid arrow.
- The node "lib" owns two children, "lexer.cpp" and "parser.cpp".
- 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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
Ownership changes hands only by moving
One report is handed out of a function, lent to another and handed over to a third:
// 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");
} 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_reportdoes; the returned local is moved, or built in place, never copied. - Take it by value to take ownership, as
archivedoes. The caller must writestd::move(q3), which makes the hand-over visible, and the report is freed whenarchivereturns. 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
printdoes: such a function uses the object and says nothing about its lifetime. - Assign a new object to a
unique_ptrthat already owns one, and the old object is deleted;reset()does the same with a raw pointer, or with no argument to leave theunique_ptrempty.
Step through main and watch who owns the report:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- make_report("Q3 sales"). main calls make_report, which creates the Report with std::make_unique: the run printed built Q3 sales.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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++”:
// 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);
} 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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- with_new(). main calls with_new(), which creates a Buffer with new and keeps its address in a plain Buffer*.
- parse throws. with_new passes the Buffer to parse, which throws std::runtime_error with the text bad input.
- 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.
- 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.
- parse throws again. parse throws the same exception as before.
- 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
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 withnew T[n]and callsdelete[]; astd::vectoris 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::fclosefor astd::FILE*. Theunique_ptrcalls it instead ofdelete, 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 aT**parameter fill aunique_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_ptrto a type that is only declared in the header.
Step through the six questions that choose among them:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- A C resource. A custom deleter, the second template argument, is called instead of delete: a std::FILE* goes back through std::fclose.
- 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.
- 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.
- 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
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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().
-
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.
-
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)>?
-
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.
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::movemarks every place where ownership changes hands. - Return a
unique_ptrby 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()andrelease()both return the raw pointer; onlyrelease()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
- Working Draft, Programming Languages: C++ (N4950): [unique.ptr], [unique.ptr.dltr.dflt], [unique.ptr.runtime], [unique.ptr.create], [out.ptr], [namespace.std], [class.base.init], [container.reqmts] (ISO/IEC JTC1/SC22/WG21)
- P1132R7: out_ptr, a scalable output pointer abstraction (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: R.3, a raw pointer (a T*) is non-owning (Standard C++ Foundation)
- C++ Core Guidelines: R.20, use unique_ptr or shared_ptr to represent ownership (Standard C++ Foundation)
- C++ Core Guidelines: R.21, prefer unique_ptr over shared_ptr unless you need to share ownership (Standard C++ Foundation)
- C++ Core Guidelines: R.23, use make_unique() to make unique_ptrs (Standard C++ Foundation)
- C++ Core Guidelines: R.32, take a unique_ptr<widget> parameter to express that a function assumes ownership (Standard C++ Foundation)
- AddressSanitizer (LLVM Project)
- getrlimit, setrlimit (RLIMIT_STACK) (The Open Group)
- pthread_attr_getstacksize, pthread_attr_setstacksize (The Open Group)
Report a problem with this lesson
Kept only in this browser. Your Learn progress