Raw pointers are powerful but dangerous. Smart pointers give you the power without the pain: automatic lifetime management, no leaks, no double-frees.
Raw pointers have an exhausting list of problems: their declaration doesn't say whether they own the thing they point to, whether you should delete them or call some other destruction mechanism, whether to use delete or delete[], whether the pointer is even valid, and whether it's been deleted already.
Smart pointers fix all of these problems. They are wrappers around raw pointers that manage lifetime automatically and make ownership semantics explicit in the type system.
std::unique_ptr (exclusive ownership), std::shared_ptr (shared ownership with reference counting), and std::weak_ptr (non-owning observer of shared_ptr). Each encodes a different ownership policy directly in the type.Three smart pointer types, three ownership models.
std::unique_ptr embodies exclusive ownership. It's the same size as a raw pointer, executes the same instructions for dereferencing, and should be your default smart pointer. When a unique_ptr is destroyed, it deletes the resource it owns.
Moving a unique_ptr transfers ownership. Copying is not allowed — if you could copy it, two unique_ptrs would think they each own the resource.
c++ // Factory function returning unique_ptr (Item 18 pattern) class Investment { public: virtual ~Investment(); }; class Stock : public Investment { }; class Bond : public Investment { }; template<typename... Ts> std::unique_ptr<Investment> makeInvestment(Ts&&... params) { // create the right derived type, return as base unique_ptr auto deleter = [](auto* p) { log(p); delete p; }; // ... (factory logic) } auto pInv = makeInvestment(/* args */); // pInv destroyed here: Investment is automatically deleted
unique_ptr accepts a custom deleter as a second template parameter. But beware: a custom deleter increases the unique_ptr's size. Stateless lambdas (no captures) add zero size. Function pointers add one pointer. std::function objects can add much more.A key feature: unique_ptr easily converts to shared_ptr, making it ideal for factory return types. Callers get exclusive ownership by default but can upgrade to shared ownership if needed.
unique_ptr the ideal return type for factory functions?shared_ptr provides shared ownership through reference counting. When the last shared_ptr to a resource is destroyed, the resource is deleted. The cost? A shared_ptr is twice the size of a raw pointer (it stores a pointer to the resource and a pointer to the control block).
Each shared_ptr points to both the resource and a control block with the reference count.
std::make_shared is called, (2) a unique_ptr is converted to shared_ptr, or (3) a shared_ptr is constructed from a raw pointer. This last case is dangerous: constructing two shared_ptrs from the same raw pointer creates two control blocks — double-free!c++ // DANGER: two control blocks for the same object! auto pw = new Widget; std::shared_ptr<Widget> spw1(pw, loggingDel); std::shared_ptr<Widget> spw2(pw, loggingDel); // double-free!
| Property | unique_ptr | shared_ptr |
|---|---|---|
| Size | Same as raw pointer | 2x raw pointer |
| Ownership | Exclusive | Shared (ref-counted) |
| Copyable | No (move only) | Yes |
| Custom deleter | Part of type | Not part of type |
| Overhead | Zero | Control block + atomic ref count |
shared_ptrs from the same raw pointer?Sometimes you need a pointer that acts like a shared_ptr but doesn't participate in ownership. weak_ptr is that pointer: it can observe a shared_ptr-managed resource without affecting its reference count.
c++ auto spw = std::make_shared<Widget>(); // ref count = 1 std::weak_ptr<Widget> wpw(spw); // ref count still 1 spw = nullptr; // ref count = 0, Widget destroyed if (wpw.expired()) { /* Widget is gone */ } // Safe access: lock() returns shared_ptr (null if expired) auto spw2 = wpw.lock(); if (spw2) { /* use *spw2 safely */ }
weak_ptrs to previously computed results; if the result is still alive, reuse it. (2) Observer pattern — subjects hold weak_ptrs to observers so they don't prevent observer destruction. (3) Breaking cycles — in a parent-child relationship where both sides use shared_ptr, one side should use weak_ptr to break the reference cycle.c++ // Caching factory pattern std::shared_ptr<Widget> fastLoadWidget(WidgetID id) { static std::unordered_map<WidgetID, std::weak_ptr<Widget>> cache; auto sp = cache[id].lock(); // try to get existing if (!sp) { sp = loadWidget(id); // not cached: load it cache[id] = sp; // cache a weak_ptr } return sp; }
weak_ptr break reference cycles between shared_ptrs?std::make_shared and std::make_unique (C++14) should be preferred over direct use of new for three reasons:
c++ // Direct new: type name written twice auto upw1(std::unique_ptr<Widget>(new Widget)); auto spw1(std::shared_ptr<Widget>(new Widget)); // make functions: type name once, exception safe auto upw2 = std::make_unique<Widget>(); auto spw2 = std::make_shared<Widget>();
processWidget(shared_ptr<Widget>(new Widget), computePriority()), the compiler may order operations as: (1) new Widget, (2) computePriority(), (3) shared_ptr constructor. If step 2 throws, the Widget from step 1 leaks. make_shared eliminates this risk.make_shared allocates the object and control block in a single allocation (one call to the allocator). Direct new + shared_ptr constructor requires two allocations.When you can't use make functions: (1) custom deleters (make functions don't support them), (2) braced initializers (make functions use parentheses internally), (3) classes with custom operator new/delete, and (4) when weak_ptrs outlive shared_ptrs (make_shared's single allocation means the memory isn't freed until the last weak_ptr dies).
make_shared more efficient than shared_ptr(new T)?The Pimpl (Pointer to IMPLementation) idiom hides a class's data members behind a pointer to a forward-declared implementation struct. This reduces compilation dependencies — changes to the implementation don't force recompilation of clients.
widget.h class Widget { public: Widget(); ~Widget(); // must be declared! Widget(Widget&&); // must be declared! Widget& operator=(Widget&&); // must be declared! private: struct Impl; // forward declaration only std::unique_ptr<Impl> pImpl; // pointer to incomplete type };
widget.cpp struct Widget::Impl { std::string name; std::vector<Gadget> gadgets; // all the heavy headers stay here }; Widget::Widget() : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() = default; // defined where Impl is complete Widget::Widget(Widget&&) = default; Widget& Widget::operator=(Widget&&) = default;
Impl is complete. The compiler-generated versions try to delete/move the unique_ptr<Impl>, which needs to see Impl's definition. If you only declare them in the header and = default them in the .cpp, it works perfectly.With shared_ptr, this isn't necessary — the deleter isn't part of the type, so the pointed-to type doesn't need to be complete at the point of destruction. But unique_ptr is the right choice here (exclusive ownership), so you pay the small cost of declaring special members.
unique_ptr?Which smart pointer should you reach for? Click each scenario to see the recommendation.
Click a scenario to see which smart pointer fits.
| Item | Key Takeaway |
|---|---|
| Item 18 | Use unique_ptr for exclusive ownership. Zero overhead. Converts to shared_ptr. |
| Item 19 | Use shared_ptr for shared ownership. 2x pointer size, atomic ref count, control block. |
| Item 20 | Use weak_ptr to observe shared_ptr without owning. Caches, observers, cycle-breaking. |
| Item 21 | Prefer make_shared/make_unique: exception safe, fewer allocations, less code. |
| Item 22 | Pimpl with unique_ptr: declare special members in header, define in .cpp. |
"When you reach for a smart pointer, std::unique_ptr should generally be the one closest at hand." — Scott Meyers