std::unique_ptr<T> is the standard library's RAII wrapper for a heap-allocated T with exclusive ownership: one pointer owns the object, and when that pointer goes out of scope, the object is deleted. It replaces the raw new/delete pair you saw earlier in this section with a type the compiler keeps honest. This chapter covers what unique_ptr is, how to construct and move it, the operations it exposes, custom deleters, and the array specialisation. Sibling chapters cover std::shared_ptr, std::weak_ptr, std::make_unique, and the rest of the smart-pointer story.
Manual new and delete are a long-running source of bugs. Forget the delete and memory leaks. Run delete twice and the heap is corrupted. Throw an exception between the new and the delete and the cleanup never happens. The RAII chapter framed the general idea; std::unique_ptr is the concrete tool the standard library provides for the most common case: "one object allocated on the heap, freed exactly once".
A leaky function that looks innocent at first glance.
The second call constructs an Order, then validate throws before delete runs, and the destructor never fires. The leak isn't a typo; the code has a clean delete at the bottom and still leaks. Any path that exits the function before that line, whether by return, by exception, or by a future maintainer adding an early return, leaks.
std::unique_ptr fixes this by tying the deletion to a stack object's lifetime, so the cleanup follows the same rules as any local variable.
Both runs destroy the Order. The throw path triggers stack unwinding, which destroys the local unique_ptr, whose destructor runs delete on the owned Order. The cleanup is no longer a separate line that can be skipped; it's wired into the local variable's lifetime.
The simplest constructor takes a raw pointer to a heap-allocated object and takes over ownership of it.
The default constructor creates an empty unique_ptr that owns nothing. It compares equal to nullptr and converts to false in a boolean context.
Two things about the raw-pointer constructor matter. First, it's explicit, so std::unique_ptr<Product> p = new Product(...) doesn't compile. The construction has to be spelled out. That's deliberate, because handing a raw pointer to a unique_ptr is the moment ownership transfers, and the language requires that to be explicit.
Second, once a raw pointer has been given to a unique_ptr, that same raw pointer must not be passed to another unique_ptr. Doing so creates two owners of the same heap object, and both will try to delete it. The result is a double-free, which corrupts the heap and usually crashes the program.
The preferred way to construct a unique_ptr is std::make_unique<T>(args...) (C++14), which forwards its arguments to T's constructor and returns a unique_ptr<T>. It avoids the explicit new entirely and is exception-safe in ways the raw constructor isn't. This chapter sticks with the raw-pointer constructor to keep the mechanics visible; a later lesson is dedicated to make_unique and the rationale.
std::unique_ptr<T> (with the default deleter) is the same size as T*. There is no extra allocation, no reference count, no atomic operation on construction or destruction. The overhead compared to a raw pointer is zero in both time and space.
A unique_ptr cannot be copied. The copy constructor and copy assignment operator are explicitly deleted. If two unique_ptr objects could point to the same heap object, both would try to delete it when they went out of scope, which is the double-free just described. The language prevents the bug at compile time.
With g++ this fails with a message like use of deleted function 'std::unique_ptr<...>::unique_ptr(const std::unique_ptr<...>&)'. The compiler refuses to copy.
The alternative is to move. Moving a unique_ptr transfers ownership: the source pointer becomes empty (a null unique_ptr), and the destination pointer takes over the owned object. Use std::move to mark the source as movable.
The owned int was never copied. The pointer value moved from a to b, and a is now empty. When b goes out of scope, the int is deleted exactly once. When a goes out of scope, its destructor runs but finds nothing to delete, which is fine.
The diagram captures what happens on a move. Cyan boxes are stack-side unique_ptr objects, orange is the heap-allocated Order, and green marks the empty state.
The heap object never moves. Only the ownership token, the pointer value, hops from a to b. After the move, a is in a valid but empty state. A new pointer can be assigned into it or it can go out of scope, but it shouldn't be dereferenced.
A natural place this shows up is a factory function that builds an Order and hands ownership back to the caller.
No std::move is needed in the return statement because the returned local is already an rvalue at that point, so the compiler picks the move constructor automatically. At the call site, order takes ownership; when main exits, the Order is destroyed.
A unique_ptr behaves like a pointer at the use site: it overloads the operators that make pointers natural and adds a few member functions for the lifecycle operations.
| Operation | What it does |
|---|---|
operator* | Dereferences to a reference to the owned object |
operator-> | Member access on the owned object |
operator bool() | true if the pointer owns an object, false if null |
get() | Returns the underlying raw pointer without giving up ownership |
release() | Returns the raw pointer and gives up ownership (caller becomes responsible for delete) |
reset(p) | Deletes the current object and takes ownership of p (defaults to nullptr) |
swap(other) | Swaps the owned pointers between two unique_ptr objects |
operator* and operator-> are the everyday operations. They make the smart pointer behave like a raw pointer at the use site.
get(), release(), and reset() are the lifecycle controls. They show up for interactions with code that takes raw pointers, or for rebinding a smart pointer to a different object.
Three details are worth pinning down.
get() is for passing the address to an API that wants a raw pointer. It does not transfer ownership. Don't call delete on what get() returns, because the unique_ptr will do that itself later, and a double-free will follow.
release() is the opposite: it hands the raw pointer back and forgets about it. The unique_ptr will not delete that object anymore. The caller now owns it and must delete it, or pass it to another owner. release() is rare in modern code but useful when interoperating with C APIs that take ownership of a pointer.
reset() deletes the currently owned object (if any) and takes over the new one passed in. Calling reset() with no argument is equivalent to reset(nullptr) and deletes whatever is owned, leaving the unique_ptr empty.
A common use of unique_ptr is as a member of another class. The owning class doesn't have to write a destructor; the unique_ptr member cleans up its owned object automatically when the owner is destroyed. This is the rule of zero in practice: when the only "resource" is something a smart pointer already manages, no destructor, copy constructor, or assignment operator is needed.
Customer doesn't declare a destructor. The default destructor that the compiler synthesises destroys the members in reverse order of declaration, which calls the unique_ptr destructor, which deletes the Address. Adding a second resource later, like a credit card record, wrapped in another unique_ptr, doesn't require writing any destructor code.
Customer itself is not copyable, because one of its members (unique_ptr) is not copyable. The compiler deletes the copy operations to keep ownership rules consistent. To pass a Customer around, move it. That's a feature, not a limitation: most domain objects with owned heap state are conceptually unique, and the compiler enforces it automatically.
Not every resource is freed with plain delete. A unique_ptr can be configured with a custom deleter that runs instead of delete when the smart pointer destroys its owned object. The deleter is the second template parameter of unique_ptr.
std::default_delete<T> calls delete on the pointer. For other cleanup, pass a different deleter type.
A common case is a C-style resource freed with a free function. C's <cstdio> returns a FILE* that must be released with std::fclose, not delete. Wrap it in a unique_ptr with a function-pointer deleter.
decltype(&std::fclose) spells out the type of the deleter (a function pointer). The second constructor argument is the actual deleter to use. When the unique_ptr is destroyed, it calls std::fclose(file) instead of delete file. Using a default-deleter unique_ptr here would call delete on a FILE*, which was never allocated with new, and the behaviour would be undefined.
A cleaner option, especially for one-off custom cleanup, is a lambda. Wrapping it in a unique_ptr requires storing the lambda type, which is unique per lambda expression and not spellable directly. Use decltype on the lambda variable to get the type.
The lambda runs in place of plain delete. It logs the deletion and then frees the object. The same pattern works with a functor (a class with operator()), which is the option to use when the deleter needs to hold state across instances.
There's one consequence to know about. The deleter is part of the unique_ptr's type. A unique_ptr<Order> (default deleter) is a different type from unique_ptr<Order, decltype(auditingDelete)>. They are not interchangeable in function signatures or containers. With the default deleter, the unique_ptr is one pointer wide; with a stateful custom deleter, it grows by the size of the deleter.
A unique_ptr with a stateless deleter (function pointer, captureless lambda, empty functor) is essentially the same size as a raw pointer thanks to the empty base optimisation. A unique_ptr with a stateful deleter is larger by the size of the deleter.
std::unique_ptr has a specialisation for arrays, written std::unique_ptr<T[]>. It calls delete[] instead of delete on destruction and supports operator[] for indexed access. Use it for a dynamically-sized C-style array; in most cases a std::vector<T> is the better choice, but the array specialisation exists for cases that need the raw layout (interop with C APIs, fixed allocation patterns).
The array specialisation does not provide operator* or operator-> (there's no single object to dereference). Element access goes through operator[]. Indexing past the end is undefined behaviour, exactly as with a raw array.
The reason this specialisation exists is the mismatch between delete and delete[]. Calling plain delete on a pointer allocated with new int[5] is undefined behaviour. The array specialisation makes the right form of deletion automatic. Without it, a custom deleter would be required every time, which is what the specialisation already provides.
A few common patterns make unique_ptr the obvious choice:
std::unique_ptr<Product> loadProduct(...) makes the ownership transfer visible, and the caller decides what to do with the returned pointer (keep it, hand it to another owner, drop it).unique_ptr member removes the need for a destructor; the default destructor cleans up correctly, and the rule of zero applies.Order subclasses (say RetailOrder, WholesaleOrder) uses std::vector<std::unique_ptr<Order>>. The vector owns the base pointers; deleting through them works as long as Order has a virtual destructor.new followed somewhere by delete almost always wants to become a single unique_ptr (or its make_unique equivalent) instead.The polymorphic-container pattern in code. It pulls together construction, move, and member-access operators.
The vector owns the smart pointers. Each smart pointer owns its concrete Order. When the vector is destroyed, every element's destructor runs, each unique_ptr calls delete on its owned object, and the virtual destructor on Order ensures the derived class's destructor runs correctly. No leaks, no manual deletes, and the ownership story is plain from the type alone.
Order declares a virtual destructor on purpose. Deleting a derived object through a base pointer without a virtual destructor is undefined behaviour, and unique_ptr<Order> does exactly that internally. Whenever derived objects are stored through a base-class unique_ptr, the base class needs virtual ~Base() = default; (or an equivalent virtual destructor).
10 quizzes