Smart Pointers
4.1 Use std::unique_ptr for Exclusive-Ownership Resource Management
1) Smart Pointer
Smart Pointer:
A class that wraps a raw pointer and automatically manages the lifetime of the resource it owns.
Smart pointers reduce resource leaks, dangling pointers, and incorrect resource destruction.
2) std::unique_ptr
std::unique_ptr:
A smart pointer that represents exclusive ownership of a resource.
A non-null std::unique_ptr owns the object it points to.
std::unique_ptr<Widget> p;
3) Exclusive Ownership
Exclusive Ownership:
An ownership model in which exactly one owner is responsible for a resource at a time.
Because ownership must remain unique, std::unique_ptr cannot be copied.
auto p1 = std::make_unique<Widget>();
// auto p2 = p1; // error
4) Move-Only Type
Move-Only Type:
A type that can transfer its state through move operations but cannot be copied.
Moving a std::unique_ptr transfers ownership to the destination and leaves the source null.
auto p1 = std::make_unique<Widget>();
auto p2 = std::move(p1);
5) Automatic Resource Destruction
When a non-null std::unique_ptr is destroyed, it automatically destroys its owned resource.
By default, destruction uses delete.
This provides RAII-based resource management.
6) Factory Function
Factory Function:
A function responsible for creating and returning objects.
std::unique_ptr is well suited as a factory return type because it clearly transfers ownership to the caller.
std::unique_ptr<Widget> createWidget();
The caller can retain exclusive ownership or later convert the pointer to std::shared_ptr.
7) Custom Deleter
Custom Deleter:
A callable object specifying how a smart pointer should destroy its resource.
auto deleter = [](Widget* p) {
delete p;
};
std::unique_ptr<Widget, decltype(deleter)>
ptr(new Widget, deleter);
The deleter type is part of the std::unique_ptr type.
8) Custom Deleter Size
With the default deleter, a std::unique_ptr is typically the same size as a raw pointer.
A function-pointer or stateful deleter may increase its size.
A stateless lambda can often be used without increasing the pointer's size.
9) Polymorphic Destruction
When a derived object is destroyed through a pointer to its base class, the base class must have a virtual destructor.
class Investment {
public:
virtual ~Investment() = default;
};
This ensures that the complete derived object is destroyed correctly.
10) Object and Array Forms
std::unique_ptr has separate forms for single objects and arrays.
std::unique_ptr<T>
std::unique_ptr<T[]>
For most arrays, containers such as std::array and std::vector are preferable.
11) Conversion to std::shared_ptr
A std::unique_ptr can transfer ownership to a std::shared_ptr.
auto up = std::make_unique<Widget>();
std::shared_ptr<Widget> sp = std::move(up);
This makes std::unique_ptr a useful default when the final ownership model is not yet known.
4.2 Use std::shared_ptr for Shared-Ownership Resource Management
1) std::shared_ptr
std::shared_ptr:
A smart pointer that allows multiple pointers to share ownership of the same resource.
The resource is destroyed when the last owning std::shared_ptr stops referring to it.
2) Shared Ownership
Shared Ownership:
An ownership model in which multiple owners collectively control the lifetime of a resource.
auto p1 = std::make_shared<Widget>();
auto p2 = p1;
Both pointers participate in ownership of the same Widget.
3) Reference Count
Reference Count:
A value that tracks how many std::shared_ptr objects currently share ownership of a resource.
Copying a std::shared_ptr normally increments the reference count.
Destroying or reassigning one normally decrements it.
When the reference count becomes zero, the managed object is destroyed.
4) Copy vs Move
Copying a std::shared_ptr requires reference-count manipulation.
auto p2 = p1;
Moving transfers the pointer without increasing the reference count.
auto p2 = std::move(p1);
Moving is therefore generally cheaper than copying.
5) Atomic Reference Counting
Reference-count operations must support concurrent access from different threads.
They therefore typically require atomic operations.
This is one source of std::shared_ptr overhead.
6) Control Block
Control Block:
A dynamically managed data structure containing information associated with shared ownership.
It typically stores:
- Reference count
- Weak count
- Custom deleter
- Custom allocator
- Other bookkeeping data
Conceptually:
std::shared_ptr<T>
├─ pointer to T ────────> T object
└─ pointer to control block
├─ reference count
├─ weak count
└─ deleter / allocator
7) std::shared_ptr Overhead
A typical std::shared_ptr contains two pointers:
- Pointer to the managed object
- Pointer to the control block
It therefore normally requires more memory and runtime bookkeeping than std::unique_ptr.
8) Custom Deleter
std::shared_ptr also supports custom deleters.
std::shared_ptr<Widget> p(
new Widget,
[](Widget* p) {
delete p;
}
);
Unlike std::unique_ptr, the deleter type is not part of the std::shared_ptr type.
9) Multiple Control Blocks
Constructing multiple independent std::shared_ptrs from the same raw pointer creates multiple control blocks.
Widget* p = new Widget;
std::shared_ptr<Widget> p1(p);
std::shared_ptr<Widget> p2(p); // wrong
Each control block believes it owns the object, which can cause the object to be destroyed multiple times.
Avoid creating std::shared_ptrs from raw pointer variables.
10) std::enable_shared_from_this
std::enable_shared_from_this:
A base-class template that allows an object already managed by std::shared_ptr to safely obtain another std::shared_ptr to itself.
class Widget
: public std::enable_shared_from_this<Widget> {
};
11) shared_from_this
shared_from_this:
A member function supplied by std::enable_shared_from_this that creates a std::shared_ptr sharing the existing control block.
auto p = shared_from_this();
It avoids incorrectly constructing a new std::shared_ptr directly from this.
12) Ownership Choice
Use std::shared_ptr only when shared ownership is actually required.
If exclusive ownership is sufficient, prefer std::unique_ptr because it has lower overhead.
A std::unique_ptr can become a std::shared_ptr, but shared ownership cannot generally be converted back into unique ownership.
4.3 Use std::weak_ptr for std::shared_ptr-Like Pointers That Can Dangle
1) std::weak_ptr
std::weak_ptr:
A non-owning smart pointer that observes an object whose lifetime is managed by std::shared_ptr.
It does not increase the object's shared reference count.
auto sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;
2) Non-Owning Reference
Non-Owning Reference:
A reference to an object that does not control or extend the object's lifetime.
A std::weak_ptr can therefore remain after the managed object has been destroyed.
3) Expired std::weak_ptr
Expired std::weak_ptr:
A std::weak_ptr whose managed object no longer exists.
if (wp.expired()) {
// object no longer exists
}
4) lock
std::weak_ptr::lock:
Atomically checks whether the object still exists and, if so, returns a std::shared_ptr owning it.
auto sp = wp.lock();
if (sp) {
// safely use object
}
If the object has already been destroyed, the returned std::shared_ptr is null.
5) Construction from std::weak_ptr
A std::shared_ptr can also be constructed directly from a std::weak_ptr.
std::shared_ptr<Widget> sp(wp);
If the std::weak_ptr has expired, this constructor throws std::bad_weak_ptr.
6) Cache
A cache can hold std::weak_ptrs when cached objects should remain alive only while other code actively owns them.
The cache can detect expired entries without extending object lifetimes.
7) Observer Pattern
Observer Pattern:
A design pattern where observer objects are notified when the state of another object changes.
Subjects can store std::weak_ptrs to observers when they need to refer to observers without owning them.
8) Circular Ownership
Circular Ownership:
A situation where objects own each other through std::shared_ptrs.
A ──shared_ptr──> B
A <──shared_ptr── B
Neither reference count reaches zero, so the objects cannot be destroyed normally.
9) Breaking Cycles
A std::weak_ptr can break a shared-ownership cycle.
A ──shared_ptr──> B
A <── weak_ptr ── B
The weak pointer does not contribute to A's shared reference count.
10) Weak Count
Weak Count:
Control-block bookkeeping associated with std::weak_ptrs referring to the control block.
std::weak_ptr does not increase the managed object's shared reference count, but it still relies on the shared control block.
4.4 Prefer std::make_unique and std::make_shared to Direct Use of new
1) std::make_unique
std::make_unique:
A function that dynamically creates an object and returns a std::unique_ptr owning it.
auto p = std::make_unique<Widget>();
It was added to the Standard Library in C++14.
2) std::make_shared
std::make_shared:
A function that dynamically creates an object and returns a std::shared_ptr owning it.
auto p = std::make_shared<Widget>();
3) Make Function
Make Function:
A function that forwards constructor arguments to a dynamically allocated object and returns a smart pointer managing that object.
The chapter discusses:
std::make_uniquestd::make_sharedstd::allocate_shared
4) Type Duplication
Direct construction repeats the object's type.
std::unique_ptr<Widget> p(new Widget);
A make function avoids the duplication.
auto p = std::make_unique<Widget>();
5) Exception Safety
Make functions improve exception safety by combining object creation with ownership establishment.
processWidget(
std::make_shared<Widget>(),
computePriority()
);
This avoids leaving a newly allocated object temporarily unmanaged if another expression throws an exception.
6) std::make_shared Allocation
Direct construction of a std::shared_ptr normally requires separate allocations for:
- The managed object
- The control block
std::shared_ptr<Widget> p(new Widget);
std::make_shared can allocate the object and control block together.
auto p = std::make_shared<Widget>();
This can reduce allocation overhead and memory usage.
7) std::allocate_shared
std::allocate_shared:
A make function similar to std::make_shared that allows a custom allocator to be supplied.
It retains the allocation advantages associated with std::make_shared.
8) Custom Deleter Limitation
Make functions do not allow a custom deleter to be specified directly.
When a custom deleter is required, direct smart-pointer construction may be necessary.
std::shared_ptr<Widget> p(
new Widget,
customDeleter
);
9) Braced Initializer Limitation
Make functions forward their arguments using parentheses.
auto p =
std::make_shared<std::vector<int>>(10, 20);
This constructs a vector containing 10 elements whose values are 20.
If construction specifically requires a braced initializer, an intermediate std::initializer_list may be needed.
10) Large Objects and std::make_shared
With std::make_shared, the object and control block usually occupy one allocation.
The object is destroyed when the shared reference count reaches zero, but the allocation may remain until the control block can also be destroyed.
If surviving std::weak_ptrs keep the control block alive, memory associated with a very large object allocation may therefore remain allocated longer.
11) Direct new When Necessary
If direct new must be used, immediately place the result into a smart pointer in a separate statement.
std::shared_ptr<Widget> p(
new Widget,
customDeleter
);
Do not leave the raw pointer temporarily unmanaged.
4.5 When Using the Pimpl Idiom, Define Special Member Functions in the Implementation File
1) Pimpl Idiom
Pimpl Idiom:
A technique that hides a class's implementation data behind a pointer to a separately defined implementation type.
Pimpl means pointer to implementation.
class Widget {
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
2) Compilation Dependency
Compilation Dependency:
A dependency that requires client code to be recompiled when an included declaration or definition changes.
Pimpl moves implementation details out of the public header, reducing these dependencies.
3) Incomplete Type
Incomplete Type:
A type that has been declared but whose full definition has not yet been seen.
struct Impl;
Pointers can be declared to incomplete types.
Impl* p;
This property is fundamental to the Pimpl Idiom.
4) Implementation Type
The complete implementation type is defined in the implementation file.
struct Widget::Impl {
std::string name;
std::vector<double> data;
};
Clients including the header do not need to see these implementation details.
5) Pimpl with std::unique_ptr
std::unique_ptr naturally represents the exclusive ownership relationship between the public object and its implementation object.
class Widget {
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
6) Destructor and Incomplete Types
The default std::unique_ptr deleter eventually applies delete to its pointer.
At the point where destruction code is generated, the pointed-to Impl type must therefore be complete.
For this reason, declare the destructor in the header:
class Widget {
public:
~Widget();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
Define it after Impl is fully defined in the implementation file.
Widget::~Widget() = default;
7) Move Operations
Declaring a destructor prevents automatic generation of move operations.
If the Pimpl class should support moving, declare the move operations explicitly.
Widget(Widget&&);
Widget& operator=(Widget&&);
Define them in the implementation file after Impl is complete.
Widget::Widget(Widget&&) = default;
Widget&
Widget::operator=(Widget&&) = default;
8) Copy Operations
std::unique_ptr itself cannot be copied.
A Pimpl class that needs copy semantics must therefore explicitly implement copying of the underlying implementation object.
Widget::Widget(const Widget& rhs)
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{}
9) Deep Copy
Deep Copy:
A copy operation that creates an independent copy of the object owned through a pointer instead of copying the pointer itself.
Pimpl classes commonly require deep copying when copy semantics are supported.
10) std::unique_ptr vs std::shared_ptr in Pimpl
std::unique_ptr is normally the appropriate choice for Pimpl because the implementation object has one exclusive owner.
With std::unique_ptr, special member functions that require destruction of Impl should be defined where Impl is complete.
std::shared_ptr does not impose the same requirement because its deleter mechanism is represented through its control block rather than as part of the smart-pointer type.
11) Pimpl Structure
A typical Pimpl design separates declarations and definitions as follows.
// widget.h
class Widget {
public:
Widget();
~Widget();
Widget(Widget&&);
Widget& operator=(Widget&&);
Widget(const Widget&);
Widget& operator=(const Widget&);
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// widget.cpp
struct Widget::Impl {
// implementation data
};
Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget() = default;
Widget::Widget(Widget&&) = default;
Widget&
Widget::operator=(Widget&&) = default;
The central purpose is to keep implementation details and their dependencies out of the public header.