Exclusive Ownership Smart Pointers
Learn modern memory management with std::unique_ptr.
What Is std::unique_ptr?
std::unique_ptr is a class template in the <memory> header that wraps one raw pointer to a heap allocation and calls delete on it from its own destructor. Its copy operations are deleted and its move operations are not, so at any moment exactly one std::unique_ptr owns the allocation. That single sentence is the whole design, and everything else in this lesson follows from it.
It helps to picture the type as a very small class you could have written yourself:
| Part of the class | What it does |
|---|---|
| One pointer member | Holds the address of the managed object, or nullptr when the handle is empty |
| Destructor | Calls delete on that pointer, unless it is null |
| Copy constructor and copy assignment | Deleted, so a second owner cannot come into existence |
| Move constructor and move assignment | Take the pointer from the source and leave the source holding nullptr |
operator*, operator->, get() |
Hand back the managed object without giving up ownership |
A
std::unique_ptr is not a pointer with extra features. It is an owner that happens to store a pointer. The pointer is the data; the ownership is the type.
Because the cleanup happens in a destructor, the handle itself has to be an object whose destructor is guaranteed to run: a local variable, a function parameter, or a data member of a class. Allocating the handle with new puts you right back where you started.
Never write
new std::unique_ptr<T>. If the handle leaks, its destructor never runs, and the object it was supposed to free leaks with it.
The Bug It Exists to Remove
A raw owning pointer needs a matching delete on every path out of a function, and functions grow paths faster than programmers add deletes.
The program below is broken: it has two exits and only one delete. Watch which line never appears in the output.
#include <iostream>
#include <string>
#include <string_view>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
void runJob(int sheets)
{
PressPlate* plate{new PressPlate{"wedding invites", 250}};
if (sheets <= 0)
{
std::cout << "nothing to print" << '\n';
return; // nothing frees this plate
}
std::cout << "printing " << sheets << " sheets" << '\n';
delete plate;
}
int main()
{
runJob(120);
runJob(0);
return 0;
}
Output:
mounted wedding invites
printing 120 sheets
washed wedding invites
mounted wedding invites
nothing to print
Two plates were mounted and only one was washed. The second runJob() call left the allocation stranded, and no compiler warning marks the spot. Add an early return for a validation failure, or a function call that can throw, and the number of exits multiplies while the single delete stays where it was.
Swapping the raw pointer for a std::unique_ptr removes one line of code and fixes every exit at once.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
void runJob(int sheets)
{
auto plate{std::make_unique<PressPlate>("wedding invites", 250)};
if (sheets <= 0)
{
std::cout << "nothing to print" << '\n';
return;
}
std::cout << "printing " << sheets << " of " << plate->impressions() << " sheets" << '\n';
}
int main()
{
runJob(120);
runJob(0);
return 0;
}
Output:
mounted wedding invites
printing 120 of 250 sheets
washed wedding invites
mounted wedding invites
nothing to print
washed wedding invites
There is no delete anywhere in runJob() now. plate is a local object, so the compiler destroys it at the closing brace and at every return in between, and destroying it destroys the PressPlate.
Building One: std::make_unique First
std::make_unique<T>(args...) allocates a T, passes args... to its constructor, and returns a std::unique_ptr<T> that already owns the result. It arrived in C++14 and it is the normal way to create one.
| Written as | What happens | When to reach for it |
|---|---|---|
auto plate{std::make_unique<PressPlate>("wedding invites", 250)}; |
Allocation and ownership happen inside one function call | Always, unless something below applies |
std::unique_ptr<PressPlate> plate{new PressPlate{"wedding invites", 250}}; |
A raw pointer exists in your code for a moment before the handle adopts it | Only when you need a custom deleter, or must adopt a pointer some other API handed you |
Create smart pointers with
std::make_unique() rather than writing new yourself. It is shorter, it names the type once instead of twice, and it never leaves a bare owning pointer lying around for you to mishandle.
Before C++17, a compiler was allowed to interleave the evaluation of function arguments. In a call like
schedule(std::unique_ptr<PressPlate>{new PressPlate{...}}, riskyCall()), it could run new, then riskyCall(), then the std::unique_ptr constructor. If riskyCall() threw, the allocation existed with nothing owning it, and it leaked. std::make_unique() is immune because the allocation and the handle are built together inside one function. C++17 tightened argument evaluation so it can no longer interleave, but the shorter, harder-to-misuse spelling is still the one to prefer.
One Owner, Enforced by the Compiler
Copying a std::unique_ptr would produce two owners of one allocation, so the standard library deletes the copy operations outright. This is not a runtime check or a convention; it is a compile error.
The program below does not compile.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
int main()
{
auto onPress{std::make_unique<PressPlate>("wedding invites", 250)};
std::unique_ptr<PressPlate> inRack{onPress};
std::cout << inRack->jobName() << '\n';
return 0;
}
The compiler reports:
s.cpp: In function 'int main()':
s.cpp:33:47: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = PressPlate; _Dp = std::default_delete<PressPlate>]'
33 | std::unique_ptr<PressPlate> inRack{onPress};
| ^
In file included from /usr/local/include/c++/16.2.0/memory:80,
from s.cpp:2:
/usr/local/include/c++/16.2.0/bits/unique_ptr.h:543:7: note: declared here
543 | unique_ptr(const unique_ptr&) = delete;
| ^~~~~~~~~~
The note points at the declaration that makes it impossible: unique_ptr(const unique_ptr&) = delete;. Copy assignment is deleted in the same way.
What you can do is move. std::move casts the named handle to an r-value, which selects move assignment, and move assignment takes the pointer and leaves the source empty.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
int main()
{
std::cout << std::boolalpha;
auto onPress{std::make_unique<PressPlate>("wedding invites", 250)};
std::unique_ptr<PressPlate> inRack{};
std::cout << "onPress owns something: " << static_cast<bool>(onPress) << '\n';
std::cout << "inRack owns something: " << static_cast<bool>(inRack) << '\n';
inRack = std::move(onPress);
std::cout << "onPress owns something: " << static_cast<bool>(onPress) << '\n';
std::cout << "inRack owns something: " << static_cast<bool>(inRack) << '\n';
std::cout << "inRack is holding " << inRack->jobName() << '\n';
return 0;
}
Output:
mounted wedding invites
onPress owns something: true
inRack owns something: false
onPress owns something: false
inRack owns something: true
inRack is holding wedding invites
washed wedding invites
Only one mounted line and one washed line appear. Nothing was duplicated and nothing was destroyed twice; the same PressPlate simply changed hands. Note also that the default-constructed inRack started out empty, which is a perfectly ordinary state for a handle to be in.
A moved-from
std::unique_ptr holds nullptr. Testing it, assigning a new value to it, and destroying it are all well defined. Dereferencing it is undefined behavior, and no diagnostic will warn you.
Reaching the Object Through the Handle
Every access to the managed object goes through one of a handful of members, and each one has a different effect on ownership.
| Expression | Yields | Ownership after | Precondition |
|---|---|---|---|
if (handle) |
true when an object is owned |
Unchanged | None |
*handle |
A reference to the managed object | Unchanged | Handle must not be empty |
handle->member |
Access to a member of the managed object | Unchanged | Handle must not be empty |
handle.get() |
The raw pointer, or nullptr if empty |
Unchanged | None |
handle.reset() |
Nothing | Handle is empty, object deleted now | None |
handle.reset(raw) |
Nothing | Handle owns raw, old object deleted |
None |
handle.release() |
The raw pointer, without deleting | Handle is empty, you must delete | None |
The bool conversion is what makes the first three rows safe to combine: test, then dereference.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
std::ostream& operator<<(std::ostream& stream, const PressPlate& plate)
{
stream << plate.jobName() << " (" << plate.impressions() << " impressions)";
return stream;
}
int main()
{
std::unique_ptr<PressPlate> plate{std::make_unique<PressPlate>("wedding invites", 250)};
if (plate)
{
std::cout << "on the bed: " << *plate << '\n';
std::cout << "sheets in this run: " << plate->impressions() << '\n';
}
plate.reset();
if (plate)
std::cout << "on the bed: " << *plate << '\n';
else
std::cout << "the bed is empty" << '\n';
std::cout << "the handle outlives the plate" << '\n';
return 0;
}
Output:
mounted wedding invites
on the bed: wedding invites (250 impressions)
sheets in this run: 250
washed wedding invites
the bed is empty
the handle outlives the plate
reset() ran the destructor immediately, in the middle of main(), which is why washed appears before the last two lines rather than at the end. The handle went on living as an empty handle, and the second if took the other branch.
release() is the one member that hands ownership back to you. It returns the raw pointer and empties the handle without deleting anything, so unless you immediately hand that pointer to another owner, you have manufactured a leak. Reach for reset() when you want the object gone.
Crossing Function Boundaries
Most confusion about std::unique_ptr is really confusion about one question: does this function need to own the object, or only to look at it? Answer that first and the signature writes itself.
| What the function needs | Signature | At the call site |
|---|---|---|
| To take ownership | void f(std::unique_ptr<T> handle) |
f(std::move(handle)) |
| Only to use the object, which may be absent | void f(const T* object) |
f(handle.get()) |
| Only to use the object, which is always present | void f(const T& object) |
f(*handle) |
| To give the caller a newly created object | std::unique_ptr<T> f() |
auto handle{f()} |
| Avoid this one | void f(const std::unique_ptr<T>&) |
It ties the function to how the caller chose to store the object |
Returning by value is safe and cheap. In C++17 and later the return is elided, so no move even takes place; in C++14 and earlier the move constructor runs. Either way the caller ends up as the sole owner, and if the caller ignores the return value the temporary is destroyed and the object is freed.
This program uses three of those rows at once.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
std::ostream& operator<<(std::ostream& stream, const PressPlate& plate)
{
stream << plate.jobName() << " (" << plate.impressions() << " impressions)";
return stream;
}
std::unique_ptr<PressPlate> mountPlate(std::string_view jobName, int impressions)
{
return std::make_unique<PressPlate>(jobName, impressions);
}
void inspectPlate(const PressPlate* plate)
{
if (plate)
std::cout << "checked " << *plate << '\n';
else
std::cout << "checked nothing, the bed is empty" << '\n';
}
void sendToPress(std::unique_ptr<PressPlate> plate)
{
if (plate)
std::cout << "printing " << *plate << '\n';
} // washing happens at this closing brace
int main()
{
std::cout << std::boolalpha;
auto plate{mountPlate("wedding invites", 250)};
inspectPlate(plate.get());
std::cout << "main still owns it: " << static_cast<bool>(plate) << '\n';
sendToPress(std::move(plate));
std::cout << "main still owns it: " << static_cast<bool>(plate) << '\n';
inspectPlate(plate.get());
return 0;
}
Output:
mounted wedding invites
checked wedding invites (250 impressions)
main still owns it: true
printing wedding invites (250 impressions)
washed wedding invites
main still owns it: false
checked nothing, the bed is empty
Three details in that run are worth pinning down. inspectPlate() never touched ownership, so main still owned the plate afterwards. sendToPress() took the handle by value, so the plate was washed at the end of sendToPress() rather than at the end of main(). And the final inspectPlate(plate.get()) was still safe on an emptied handle, because get() on an empty handle returns nullptr and the function checks for it.
inspectPlate() takes a const PressPlate*, not a const std::unique_ptr<PressPlate>&. That is deliberate. A function that only reads the object should not care whether the caller keeps it in a smart pointer, on the stack, or inside a container.
Holding a Handle Inside a Class
A std::unique_ptr data member turns a class into an owner without you writing a destructor at all.
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
class PressPlate
{
public:
PressPlate(std::string_view jobName, int impressions)
: m_jobName{jobName}
, m_impressions{impressions}
{
std::cout << "mounted " << m_jobName << '\n';
}
~PressPlate()
{
std::cout << "washed " << m_jobName << '\n';
}
const std::string& jobName() const { return m_jobName; }
int impressions() const { return m_impressions; }
private:
std::string m_jobName{};
int m_impressions{0};
};
class PressRun
{
public:
PressRun(std::string_view jobName, int impressions)
: m_plate{std::make_unique<PressPlate>(jobName, impressions)}
{
}
void report() const
{
if (m_plate)
std::cout << "scheduled " << m_plate->jobName() << '\n';
}
private:
std::unique_ptr<PressPlate> m_plate{};
};
int main()
{
{
PressRun morning{"wedding invites", 250};
morning.report();
}
std::cout << "shop floor clear" << '\n';
return 0;
}
Output:
mounted wedding invites
scheduled wedding invites
washed wedding invites
shop floor clear
PressRun declares no destructor. When morning goes out of scope, the implicitly generated destructor destroys each member, and destroying m_plate destroys the PressPlate.
The member also changes what PressRun itself can do. Since std::unique_ptr cannot be copied, the compiler cannot generate a copy constructor or copy assignment for PressRun either, so PressRun becomes a move-only type. That is usually exactly right: if the run owns its plate exclusively, two runs must not claim the same plate.
The member only helps if the enclosing object is itself destroyed properly. A
PressRun allocated with new and never deleted leaks its PressPlate too, because the member's destructor never runs.
Wrapping Arrays, and Why You Rarely Should
std::unique_ptr<T[]> is a separate partial specialization. It calls delete[] instead of delete, and it gives you operator[] instead of operator* and operator->.
#include <cstddef>
#include <iostream>
#include <memory>
int main()
{
auto sheetCounts{std::make_unique<int[]>(3)};
sheetCounts[0] = 250;
sheetCounts[1] = 480;
for (std::size_t index{0}; index < 3; ++index)
std::cout << "run " << index << ": " << sheetCounts[index] << '\n';
return 0;
}
Output:
run 0: 250
run 1: 480
run 2: 0
It works, and the elements were value-initialized to zero, but notice what it does not give you: no size to query, no bounds-aware iteration, no growth, no begin() and end().
Reach for
std::vector, std::array, or std::string before a smart pointer wrapped around an array. They own their memory just as safely and they know how many elements they have.
Two Ways to Break Exclusive Ownership
Both fragments below are broken, and both come from the same root cause: a raw owning pointer that has a name of its own, so it can be used twice.
The first mistake is handing the same raw pointer to two handles.
PressPlate* raw{new PressPlate{"wedding invites", 250}};
std::unique_ptr<PressPlate> first{raw};
std::unique_ptr<PressPlate> second{raw}; // two owners, one allocation
The second mistake, equally broken, is deleting the object yourself while a handle still owns it.
PressPlate* raw{new PressPlate{"wedding invites", 250}};
std::unique_ptr<PressPlate> holder{raw};
delete raw; // holder will delete it a second time
Both fragments compile without a single warning, and both produce undefined behavior when they run, because the same allocation is freed twice. Neither program's output is shown here for that reason: a program with undefined behavior has no output worth quoting.
Exclusive ownership is only exclusive if nothing else holds an owning pointer to the same object. Once you have written
new into a named raw pointer, the compiler can no longer help you.
std::make_unique() prevents both mistakes for free, because the raw pointer it allocates is never given a name you could reuse. That is the strongest practical argument for the factory function, beyond the typing it saves.
std::auto_ptr was an earlier attempt at the same idea, written before the language had move semantics. It made copying look legal while silently emptying the object it copied from, which broke in surprising ways. It was deprecated in C++11 and removed in C++17. std::unique_ptr is its replacement, and it works because deleting the copy operations is now something a class can actually do.
Summary
std::unique_ptr from <memory> owns one heap allocation exclusively and frees it from its destructor. Copying is deleted, moving transfers the pointer and empties the source, and the destructor runs on every exit path from the enclosing scope, which is why the leak in the first program cannot happen once a handle is doing the owning.
| Question | Answer |
|---|---|
| How do I create one? | std::make_unique<T>(args...), since C++14 |
| Can I copy it? | No. Copy construction and copy assignment are deleted |
| How do I transfer it? | std::move, which selects the move operation |
| How do I know it holds something? | The bool conversion: if (handle) |
| How do I use the object? | *handle and handle->member, after testing the handle |
| How do I lend the object out? | Pass handle.get() or *handle, not the handle itself |
| How do I return one? | By value. C++17 elides the return; C++14 and earlier move it |
| How do I destroy it early? | reset(). Never delete handle.get() |
| What about arrays? | std::unique_ptr<T[]> exists, but std::vector is the better answer |
Use
std::unique_ptr whenever a heap-allocated object has exactly one owner, build it with std::make_unique(), move it when ownership changes hands, and pass the plain object or pointer to any function that only needs to read it.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Exclusive Ownership Smart Pointers - Quiz
Test your understanding of the lesson.
Practice Exercises
std::unique_ptr Basics
Learn to use std::unique_ptr for exclusive ownership of dynamically allocated resources. Practice creating, transferring, and using unique pointers.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!