std::shared_ptrs offer convenience approaching that of garbage collection for the shared lifetime management of arbitrary resources.
Some facts
std::shared_ptrs are twice the size of a raw pointer:- a raw pointer to the resource
- a raw pointer to the resource’s control block, which is dynamically allocated and contains
- reference count
- weak count
- other optional data (e.g., custom deleter, allocator, etc)
- increments and decrements of the reference count are atomic
- reading and writing reference counts is typically slower than non-atomic operations and comparatively costly
- the related atomic operations typically map to individual machine instructions. Admittedly, they are expensive compared to non-atomic instructons, but they’re still just single instructions
- move-constructing a
std::shared_ptrfrom anotherstd::shared_ptrrequires no refernece count manipulation
- Default resource destruction is via
delete, but custom deleters are supported- the type of the deleter has no effect on the type of the
std::shared_ptr(they’re in the control block)
- the type of the deleter has no effect on the type of the
std::shared_ptris designed only for pointers to single objects, and can’t work with arrays, unlikestd::unique_ptr
Basically, this is how a std::shared_ptr<T> object looks like in the memory:
std::shared_ptr<T>
┌──────────────────────┐ ┌──────────┐
│ Ptr to T ├──>│ T Object │
├──────────────────────┤ └──────────┘
│ Ptr to Control Block ├───┐
└──────────────────────┘ ↓ Control Block
┌───────────────────────┐
│ Reference Count │
├───────────────────────┤
│ Weak Count │
├───────────────────────┤
│ Other Data │
│ (e.g., custom deleter,│
│ allocator, etc.) │
└───────────────────────┘
It is worth noting that std::shared_ptr design is more flexible than std::unique_ptr in the aspect of specifying custom deleters, because, unlike std::unique_ptr, the type of the deleter is not part of the type of the std::shared_ptr:
|
|
Apart from placing spw1 and spw2 in the same container, we could also assign one to another, or pass them into a function taking a parameter of type std::shared_ptr<Widget>. None of these things can be done with std::unique_ptrs, since their types differ due to different custom deleters.
Avoid multiple control blocks from this pointer
Below is the rules to create the control block:
- a control block is created when
std::make_shared(EMCpp item 20) is called (which manufactures a new object to point to) - a control block is created when
std::shared_ptris constructed from a unique-ownership pointer (i.e., astd::unique_ptrorstd::auto_ptr), and the unique-ownership pointer is set to null later. - a control block is created when a
std::shared_ptris called with a raw pointer- this may lead to double deletion issue (creating two control blocks with the same raw pointer), so avoid passing raw pointers to a
std::shared_ptr - if have to, do in this form
std::shared_ptr<Widget> spw(new Widget, loggingDel);to usenewdirectly
- this may lead to double deletion issue (creating two control blocks with the same raw pointer), so avoid passing raw pointers to a
- the same control block will be shared if a
std::shared_ptris constructed using anotherstd::shared_ptras initialization argument (by callingstd::shared_ptrcopy constructor)
Even though we remember the rule to restrict the creation of std::shared_ptr from raw pointers, we might surprisingly create multiple control blocks through the this pointer, which is also a raw pointer. For example, Widget has a member function void processing();, and we use a vector to keep track of Widgets that have been processed. A reasonable-looking approach looks like this:
|
|
This code will compiler, but it has potential danger: process() passes raw pointer this to a container of std::shared_ptr, resulting to a new control block for pointed-to Widget(*this); as long as there are std::shared_ptrs outside the process() that already point to that Widget, undefined behavior shows up.
Here, we want a class managed by std::shared_ptrs to be able to safely create a std::shared_ptr from a this pointer without creating multiple control blocks. The solution is to inherit from a base class template std::enable_shared_from_this, which contains a member function shared_from_this(), which points to the same object as the this pointer. We can use this function to create a std::shared_ptr worring about creating a new control block.
|
|
Two points worth noting:
- Here, the derived class(
Widget) inherites from a base class templatized on the derived class. This design pattern is called CRTP: The Curiously Recurring Template Pattern. - Internally,
shared_from_this()looks up the control block for the current object, and it creates a newstd::shared_ptrreferring to that control block. This means there must be an existingstd::shared_ptrthat points to the current object. If no suchstd::shared_ptrexists, behavior is undefined (typically an exception will be thrown)
To prevent invoking shared_from_this() before a std::shared_ptr points to the object, classes inheriting from std::enable_shared_from_this often declare their constructors private and provide factory functions returning std::shared_ptrs to clients:
|
|