unique_ptr — Exclusive Ownership
std::unique_ptr<T>is the standard library's exclusive-ownership smart pointer. It wraps a raw pointer and automatically calls deletewhen it goes out of scope, making heap-allocated objects as safe as stack-allocated ones. The “unique” in its name means exactly one unique_ptr owns any given object at a time — it cannot be copied, only moved. This single constraint eliminates double-free bugs and most memory leaks at zero runtime cost.
Creating a unique_ptr
Always create unique_ptr objects using std::make_unique<T>(args...) (C++14), not by wrapping a raw pointer manually. make_unique is exception-safe and prevents the raw pointer from ever appearing in user code, closing the last window for accidental double-ownership.
#include <memory>
// Preferred: make_unique forwards args to the constructor
auto p = std::make_unique<int>(42);
auto s = std::make_unique<std::string>("hello");
auto v = std::make_unique<std::vector<int>>(10, 0); // 10 zeros
// Also creates an array (note: delete[] is called automatically):
auto buf = std::make_unique<char[]>(1024);
// Avoid this pattern (raw new leaks if an exception happens between new and the unique_ptr ctor):
// std::unique_ptr<int> p(new int(42)); // fragile — prefer make_uniqueAccessing the managed object
unique_ptr overloads * and -> to behave like a raw pointer. get() returns the underlying raw pointer without transferring ownership — use it when you need to pass the address to a function that expects a raw pointer (typically a C API).
struct Widget { int value; void draw(); };
auto w = std::make_unique<Widget>();
w->value = 42; // access member via ->
(*w).draw(); // dereference then member access
// get() for interfacing with legacy APIs:
void legacy_api(Widget* p);
legacy_api(w.get()); // raw pointer, w retains ownership
// Boolean test — false if null:
if (w) {
std::cout << "has object\n";
}
// Release ownership (gives you the raw pointer, unique_ptr becomes null):
Widget* raw = w.release(); // w is now null; you must delete raw manually
delete raw; // your responsibility nowTransferring ownership with move
unique_ptr is move-only: copying it is a compile error because two pointers to the same object would both try to delete it. Moving it transfers ownership: the source becomes null and the destination takes over the object. This makes ownership transfer explicit and visible in the code.
auto a = std::make_unique<int>(10);
// auto b = a; // COMPILE ERROR: copy is deleted
auto b = std::move(a); // ownership transferred: a is now null
std::cout << (a == nullptr); // true — a no longer owns anything
std::cout << *b; // 10 — b owns the int
// Passing unique_ptr to a function = transferring ownership:
void take_ownership(std::unique_ptr<Widget> w) {
w->draw();
// w destroyed at end — Widget is deleted
}
auto widget = std::make_unique<Widget>();
take_ownership(std::move(widget)); // widget is null after this
// Returning unique_ptr from a function is fine — implicit move:
std::unique_ptr<Widget> make_widget() {
return std::make_unique<Widget>(); // no std::move needed — RVO applies
}Ownership patterns: sink, borrow, observe
The right function signature depends on what the function does with the object:
Sink (take ownership)
void f(std::unique_ptr<T> p)The function takes ownership. Caller must pass std::move(ptr). Used for constructors or factories that store the object permanently.
Borrow (use but not own, may modify)
void f(T& obj) or void f(T* p)The function uses the object but doesn't own it. Pass *ptr or ptr.get(). This is the most common pattern for utility functions.
Observe (read-only, not own)
void f(const T& obj) or void f(const T* p)The function reads the object. Pass *ptr or ptr.get(). The unique_ptr is irrelevant — the function doesn't need to know about the ownership model.
The core principle: functions that don't need to participate in ownership should receive raw references or pointers, not unique_ptr. Passing unique_ptrby reference to a function that doesn't take ownership is unnecessary coupling.
unique_ptr and polymorphism
unique_ptris the standard tool for storing polymorphic objects — objects whose concrete type isn't known at compile time. A container of unique_ptr<Base> can hold any derived type, with virtual dispatch working through the pointer.
struct Shape { virtual void draw() const = 0; virtual ~Shape() = default; };
struct Circle : Shape { void draw() const override { std::cout << "O\n"; } };
struct Square : Shape { void draw() const override { std::cout << "[]\n"; } };
// Heterogeneous collection of shapes:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());
shapes.push_back(std::make_unique<Circle>());
for (const auto& s : shapes) {
s->draw(); // virtual dispatch: O, [], O
}
// All shapes deleted automatically when shapes goes out of scopeKey rules to remember
Use make_unique, never raw new
make_unique is exception-safe and keeps raw pointers out of user code. raw new can leak if an exception is thrown before the unique_ptr is constructed.
unique_ptr is move-only — copying is a compile error by design
This enforces single ownership. To transfer, use std::move. The source becomes null — it no longer has any object.
Pass unique_ptr by value only when transferring ownership; otherwise pass the underlying T& or T*
Functions that just use the object don't need to know about the ownership model. Get the object out with *ptr or ptr.get().
unique_ptr has zero runtime overhead versus raw new/delete
The destructor is inlined and the size is the same as a raw pointer (when using the default deleter). There is no reference counting, no heap allocation for the control block.