Chapter 4. Smart Pointers
Poets and songwriters have a thing about love. And sometimes about counting. Occasionally both. Inspired by the rather different takes on love and counting by Elizabeth Barrett Browning (âHow do I love thee? Let me count the ways.â) and Paul Simon (âThere must be 50 ways to leave your lover.â), we might try to enumerate the reasons why a raw pointer is hard to love:
-
Its declaration doesnât indicate whether it points to a single object or to an array.
-
Its declaration reveals nothing about whether you should destroy what it points to when youâre done using it, i.e., if the pointer owns the thing it points to.
-
If you determine that you should destroy what the pointer points to, thereâs no way to tell how. Should you use
delete
, or is there a different destruction mechanism (e.g., a dedicated destruction function the pointer should be passed to)? -
If you manage to find out that
delete
is the way to go, Reason 1 means it may not be possible to know whether to use the single-object form (âdelete
â) or the array form (âdelete
[]
â). If you use the wrong form, results are undefined. -
Assuming you ascertain that the pointer owns what it points to and you discover how to destroy it, itâs difficult to ensure that you perform the destruction exactly once along every path in your code (including those due to exceptions). Missing a path leads to resource leaks, and doing the destruction more than once leads to undefined behavior.
-
Thereâs typically no way to tell if the pointer dangles, i.e., points to memory that no longer holds the object the pointer is supposed to point to. Dangling pointers arise when objects are destroyed while pointers still point to them.
Raw pointers are powerful tools, to be sure, but decades of experience have demonstrated that with only the slightest lapse in concentration or discipline, these tools can turn on their ostensible masters.
Smart pointers are one way to address these issues. Smart pointers are wrappers around raw pointers that act much like the raw pointers they wrap, but that avoid many of their pitfalls. You should therefore prefer smart pointers to raw pointers. Smart pointers can do virtually everything raw pointers can, but with far fewer opportunities for error.
There are four smart pointers in C++11: std::auto_ptr
, std::unique_ptr
, std::shared_ptr
, and std::weak_ptr
. All are designed to help manage the lifetimes of dynamically allocated objects, i.e., to avoid resource leaks by ensuring that such objects are destroyed in the appropriate manner at the appropriate time (including in the event of exceptions).
std::auto_ptr
is a deprecated leftover from C++98. It was an attempt to standardize what later became C++11âs std::unique_ptr
. Doing the job right required move semantics, but C++98 didnât have them. As a workaround, std::auto_ptr
co-opted its copy operations for moves. This led to surprising code (copying a std::auto_ptr
sets it to null!) and frustrating usage restrictions (e.g., it wasnât possible to store std::auto_ptr
s in containers).
std::unique_ptr
does everything std::auto_ptr
does, plus more. It does it as efficiently, and it does it without warping what it means to copy an object. Itâs better than std::auto_ptr
in every way. The only legitimate use case for std::auto_ptr
is a need to compile code with C++98 compilers. Unless you have that constraint, you should replace std::auto_ptr
with std::unique_ptr
and never look back.
The smart pointer APIs are remarkably varied. About the only functionality common to all is default construction. Because comprehensive references for these APIs are widely available, Iâll focus my discussions on information thatâs often missing from API overviews, e.g., noteworthy use cases, runtime cost analyses, etc. Mastering such information can be the difference between merely using these smart pointers and using them effectively.
Item 18:âUse std::unique_ptr
for exclusive-ownership resource management.
When you reach for a smart pointer, std::unique_ptr
should generally be the one closest at hand. Itâs reasonable to assume that, by default, std::unique_ptr
s are the same size as raw pointers, and for most operations (including dereferencing), they execute exactly the same instructions. This means you can use them even in situations where memory and cycles are tight. If a raw pointer is small enough and fast enough for you, a std::unique_ptr
almost certainly is, too.
std::unique_ptr
embodies exclusive ownership semantics. A non-null std::
unique_ptr
always owns what it points to. Moving a std::unique_ptr
transfers ownership from the source pointer to the destination pointer. (The source pointer is set to null.) Copying a std::unique_ptr
isnât allowed, because if you could copy a std::unique_ptr
, youâd end up with two std::unique_ptr
s to the same resource, each thinking it owned (and should therefore destroy) that resource. std::unique_ptr
is thus a move-only type. Upon destruction, a non-null std::unique_ptr
destroys its resource. By default, resource destruction is accomplished by applying delete
to the raw pointer inside the std::unique_ptr
.
A common use for std::unique_ptr
is as a factory function return type for objects in a hierarchy. Suppose we have a hierarchy for types of investments (e.g., stocks, bonds, real estate, etc.) with a base class Investment
.
class Investment { ⦠}; class Stock: public Investment { ⦠}; class Bond: public Investment { ⦠}; class RealEstate: public Investment { ⦠};
A factory function for such a hierarchy typically allocates an object on the heap and returns a pointer to it, with the caller being responsible for deleting the object when itâs no longer needed. Thatâs a perfect match for std::unique_ptr
, because the caller acquires responsibility for the resource returned by the factory (i.e., exclusive ownership of it), and the std::unique_ptr
automatically deletes what it points to when the std::unique_ptr
is destroyed. A factory function for the Investment
hierarchy could be declared like this:
template<typename... Ts> // return std::unique_ptr std::unique_ptr<Investment> // to an object created makeInvestment(Ts&&... params); // from the given args
Callers could use the returned std::unique_ptr
in a single scope as follows,
{ ⦠auto pInvestment = // pInvestment is of type makeInvestment( arguments ); // std::unique_ptr<Investment> ⦠} // destroy *pInvestment
but they could also use it in ownership-migration scenarios, such as when the std::unique_ptr
returned from the factory is moved into a container, the container element is subsequently moved into a data member of an object, and that object is later destroyed. When that happens, the objectâs std::unique_ptr
data member would also be destroyed, and its destruction would cause the resource returned from the factory to be destroyed. If the ownership chain got interrupted due to an exception or other atypical control flow (e.g., early function return or break
from a loop), the std::unique_ptr
owning the managed resource would eventually have its destructor called,1 and the resource it was managing would thereby be destroyed.
By default, that destruction would take place via delete
, but, during construction, std::unique_ptr
objects can be configured to use custom deleters: arbitrary functions (or function objects, including those arising from lambda expressions) to be invoked when itâs time for their resources to be destroyed. If the object created by makeInvestment
shouldnât be directly delete
d, but instead should first have a log entry written, makeInvestment
could be implemented as follows. (An explanation follows the code, so donât worry if you see something whose motivation is less than obvious.)
auto delInvmt = [](Investment* pInvestment) // custom { // deleter makeLogEntry(pInvestment); // (a lambda delete pInvestment; // expression) }; template<typename... Ts> // revised std::unique_ptr<Investment, decltype(delInvmt)> // return type makeInvestment(Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> // ptr to be pInv(nullptr, delInvmt); // returned if ( /* a Stock object should be created */ ) { pInv.reset(new Stock(std::forward<Ts>(params)...)); } else if ( /* a Bond object should be created */ ) { pInv.reset(new Bond(std::forward<Ts>(params)...)); } else if ( /* a RealEstate object should be created */ ) { pInv.reset(new RealEstate(std::forward<Ts>(params)...)); } return pInv; }
In a moment, Iâll explain how this works, but first consider how things look if youâre a caller. Assuming you store the result of the makeInvestment
call in an auto
variable, you frolic in blissful ignorance of the fact that the resource youâre using requires special treatment during deletion. In fact, you veritably bathe in bliss, because the use of std::unique_ptr
means you need not concern yourself with when the resource should be destroyed, much less ensure that the destruction happens exactly once along every path through the program. std::unique_ptr
takes care of all those things automatically. From a clientâs perspective, makeInvestment
âs interface is sweet.
The implementation is pretty nice, too, once you understand the following:
-
delInvmt
is the custom deleter for the object returned frommakeInvestment
. All custom deletion functions accept a raw pointer to the object to be destroyed, then do what is necessary to destroy that object. In this case, the action is to callmakeLogEntry
and then applydelete
. Using a lambda expression to createdelInvmt
is convenient, but, as weâll see shortly, itâs also more efficient than writing a conventional function. -
When a custom deleter is to be used, its type must be specified as the second type argument to
std::unique_ptr
. In this case, thatâs the type ofdelInvmt
, and thatâs why the return type ofmakeInvestment
isstd::unique_ptr<Investment,
decltype(delInvmt)>
. (For information aboutdecltype
, see Item 3.) -
The basic strategy of
makeInvestment
is to create a nullstd::unique_ptr
, make it point to an object of the appropriate type, and then return it. To associate the custom deleterdelInvmt
withpInv
, we pass that as its second constructor argument. -
Attempting to assign a raw pointer (e.g., from
new
) to astd::unique_ptr
wonât compile, because it would constitute an implicit conversion from a raw to a smart pointer. Such implicit conversions can be problematic, so C++11âs smart pointers prohibit them. Thatâs whyreset
is used to havepInv
assume ownership of the object created vianew
. -
With each use of
new
, we usestd::forward
to perfect-forward the arguments passed tomakeInvestment
(see Item 25). This makes all the information provided by callers available to the constructors of the objects being created. -
The custom deleter takes a parameter of type
Investment*
. Regardless of the actual type of object created insidemakeInvestment
(i.e.,Stock
,Bond
, orRealEstate
), it will ultimately bedelete
d inside the lambda expression as anInvestment*
object. This means weâll be deleting a derived class object via a base class pointer. For that to work, the base classâInvestment
âmust have a virtual destructor:class Investment { public: ⦠// essential virtual ~Investment(); // design ⦠// component! };
In C++14, the existence of function return type deduction (see Item 3) means that makeInvestment
could be implemented in this simpler and more encapsulated fashion:
template<typename... Ts> auto makeInvestment(Ts&&... params) // C++14 { auto delInvmt = [](Investment* pInvestment) // this is now { // inside makeLogEntry(pInvestment); // make- delete pInvestment; // Investment }; std::unique_ptr<Investment, decltype(delInvmt)> // as pInv(nullptr, delInvmt); // before if ( ⦠) // as before { pInv.reset(new Stock(std::forward<Ts>(params)...)); } else if ( ⦠) // as before { pInv.reset(new Bond(std::forward<Ts>(params)...)); } else if ( ⦠) // as before { pInv.reset(new RealEstate(std::forward<Ts>(params)...)); } return pInv; // as before }
I remarked earlier that, when using the default deleter (i.e., delete
), you can reasonably assume that std::unique_ptr
objects are the same size as raw pointers. When custom deleters enter the picture, this may no longer be the case. Deleters that are function pointers generally cause the size of a std::unique_ptr
to grow from one word to two. For deleters that are function objects, the change in size depends on how much state is stored in the function object. Stateless function objects (e.g., from lambda expressions with no captures) incur no size penalty, and this means that when a custom deleter can be implemented as either a function or a captureless lambda expression, the lambda is preferable:
auto delInvmt1 = [](Investment* pInvestment) // custom { // deleter makeLogEntry(pInvestment); // as delete pInvestment; // stateless }; // lambda template<typename... Ts> // return type std::unique_ptr<Investment, decltype(delInvmt1)> // has size of makeInvestment(Ts&&... args); // Investment* void delInvmt2(Investment* pInvestment) // custom { // deleter makeLogEntry(pInvestment); // as function delete pInvestment; } template<typename... Ts> // return type has std::unique_ptr<Investment, // size of Investment* void (*)(Investment*)> // plus at least size makeInvestment(Ts&&... params); // of function pointer!
Function object deleters with extensive state can yield std::unique_ptr
objects of significant size. If you find that a custom deleter makes your std::unique_ptr
s unacceptably large, you probably need to change your design.
Factory functions are not the only common use case for std::unique_ptr
s. Theyâre even more popular as a mechanism for implementing the Pimpl Idiom. The code for that isnât complicated, but in some cases itâs less than straightforward, so Iâll refer you to Item 22, which is dedicated to the topic.
std::unique_ptr
comes in two forms, one for individual objects (std::
unique_ptr<T>
) and one for arrays (std::unique_ptr<T[]>
). As a result, thereâs never any ambiguity about what kind of entity a std::unique_ptr
points to. The std::unique_ptr
API is designed to match the form youâre using. For example, thereâs no indexing operator (operator[]
) for the single-object form, while the array form lacks dereferencing operators (operator*
and operator->
).
The existence of std::unique_ptr
for arrays should be of only intellectual interest to you, because std::array
, std::vector
, and std::string
are virtually always better data structure choices than raw arrays. About the only situation I can conceive of when a std::unique_ptr<T[]>
would make sense would be when youâre using a C-like API that returns a raw pointer to a heap array that you assume ownership of.
std::unique_ptr
is the C++11 way to express exclusive ownership, but one of its most attractive features is that it easily and efficiently converts to a std::
shared_ptr
:
std::shared_ptr<Investment> sp = // converts std::unique_ptr makeInvestment( arguments ); // to std::shared_ptr
This is a key part of why std::unique_ptr
is so well suited as a factory function return type. Factory functions canât know whether callers will want to use exclusive-ownership semantics for the object they return or whether shared ownership (i.e., std::shared_ptr
) would be more appropriate. By returning a std::unique_ptr
, factories provide callers with the most efficient smart pointer, but they donât hinder callers from replacing it with its more flexible sibling. (For information about std::shared_ptr
, proceed to Item 19.)
Things to Remember
-
std::unique_ptr
is a small, fast, move-only smart pointer for managing resources with exclusive-ownership semantics. -
By default, resource destruction takes place via
delete
, but custom deleters can be specified. Stateful deleters and function pointers as deleters increase the size ofstd::unique_ptr
objects.
Item 22:âWhen using the Pimpl Idiom, define special member functions in the implementation file.
If youâve ever had to combat excessive build times, youâre familiar with the Pimpl (âpointer to implementationâ) Idiom. Thatâs the technique whereby you replace the data members of a class with a pointer to an implementation class (or struct), put the data members that used to be in the primary class into the implementation class, and access those data members indirectly through the pointer. For example, suppose Widget
looks like  this:
class Widget { // in header "widget.h" public: Widget(); ⦠private: std::string name; std::vector<double> data; Gadget g1, g2, g3; // Gadget is some user- }; // defined type
Because Widget
âs data members are of types std::string
, std::vector
, and Gadget
, headers for those types must be present for Widget
to compile, and that means that Widget
clients must #include
<string>
, <vector>
, and gadget.h
. Those headers increase the compilation time for Widget
clients, plus they make those clients dependent on the contents of the headers. If a headerâs content changes, Widget
clients must recompile. The standard headers <string>
and <vector>
donât change very often, but it could be that gadget.h
is subject to frequent revision.
Applying the Pimpl Idiom in C++98 could have Widget
replace its data members with a raw pointer to a struct that has been declared, but not defined:
class Widget { // still in header "widget.h" public: Widget(); ~Widget(); // dtor is neededâsee below ⦠private: struct Impl; // declare implementation struct Impl *pImpl; // and pointer to it };
Because Widget
no longer mentions the types std::string
, std::vector
, and Gadget
, Widget
clients no longer need to #include
the headers for these types. That speeds compilation, and it also means that if something in these headers changes, Widget
clients are unaffected.
A type that has been declared, but not defined, is known as an incomplete type. Widget::Impl
is such a type. There are very few things you can do with an incomplete type, but declaring a pointer to it is one of them. The Pimpl Idiom takes advantage of that.
Part 1 of the Pimpl Idiom is the declaration of a data member thatâs a pointer to an incomplete type. Part 2 is the dynamic allocation and deallocation of the object that holds the data members that used to be in the original class. The allocation and deallocation code goes in the implementation file, e.g., for Widget
, in widget.cpp
:
#include "widget.h" // in impl. file "widget.cpp" #include "gadget.h" #include <string> #include <vector> struct Widget::Impl { // definition of Widget::Impl std::string name; // with data members formerly std::vector<double> data; // in Widget Gadget g1, g2, g3; }; Widget::Widget() // allocate data members for : pImpl(new Impl) // this Widget object {} Widget::~Widget() // destroy data members for { delete pImpl; } // this object
Here Iâm showing #include
directives to make clear that the overall dependencies on the headers for std::string
, std::vector
, and Gadget
continue to exist. However, these dependencies have been moved from widget.h
(which is visible to and used by Widget
clients) to widget.cpp
(which is visible to and used only by the Widget
implementer). Iâve also highlighted the code that dynamically allocates and deallocates the Impl
object. The need to deallocate this object when a Widget
is destroyed is what necessitates the Widget
destructor.
But Iâve shown you C++98 code, and that reeks of a bygone millennium. It uses raw pointers and raw new
and raw delete
and itâs all just soâ¦raw. This chapter is built on the idea that smart pointers are preferable to raw pointers, and if what we want is to dynamically allocate a Widget::Impl
object inside the Widget
constructor and have it destroyed at the same time the Widget
is, std::unique_ptr
(see Item 18) is precisely the tool we need. Replacing the raw pImpl
pointer with a std::unique_ptr
yields this code for the header file,
class Widget { // in "widget.h" public: Widget(); ⦠private: struct Impl; std::unique_ptr<Impl> pImpl; // use smart pointer }; // instead of raw pointer
and this for the implementation file:
#include "widget.h" // in "widget.cpp" #include "gadget.h" #include <string> #include <vector> struct Widget::Impl { // as before std::string name; std::vector<double> data; Gadget g1, g2, g3; }; Widget::Widget() // per Item 21, create : pImpl(std::make_unique<Impl>()) // std::unique_ptr {} // via std::make_unique
Youâll note that the Widget
destructor is no longer present. Thatâs because we have no code to put into it. std::unique_ptr
automatically deletes what it points to when it (the std::unique_ptr
) is destroyed, so we need not delete anything ourselves. Thatâs one of the attractions of smart pointers: they eliminate the need for us to sully our hands with manual resource release.
This code compiles, but, alas, the most trivial client use doesnât:
#include "widget.h"
Widget w; // error!
The error message you receive depends on the compiler youâre using, but the text generally mentions something about applying sizeof
or delete
to an incomplete type. Those operations arenât among the things you can do with such types.
This apparent failure of the Pimpl Idiom using std::unique_ptr
s is alarming, because (1) std::unique_ptr
is advertised as supporting incomplete types, and (2) the Pimpl Idiom is one of std::unique_ptr
s most common use cases. Fortunately, getting the code to work is easy. All thatâs required is a basic understanding of the cause of the problem.
The issue arises due to the code thatâs generated when w
is destroyed (e.g., goes out of scope). At that point, its destructor is called. In the class definition using std::unique_ptr
, we didnât declare a destructor, because we didnât have any code to put into it. In accord with the usual rules for compiler-generated special member functions (see Item 17), the compiler generates a destructor for us. Within that destructor, the compiler inserts code to call the destructor for Widget
âs data member pImpl
. pImpl
is a std::unique_ptr<Widget::Impl>
, i.e., a std::unique_ptr
using the default deleter. The default deleter is a function that uses delete
on the raw pointer inside the std::unique_ptr
. Prior to using delete
, however, implementations typically have the default deleter employ C++11âs static_assert
to ensure that the raw pointer doesnât point to an incomplete type. When the compiler generates code for the destruction of the Widget
w
, then, it generally encounters a static_assert
that fails, and thatâs usually what leads to the error message. This message is associated with the point where w
is destroyed, because Widget
âs destructor, like all compiler-generated special member functions, is implicitly inline
. The message itself often refers to the line where w
is created, because itâs the source code explicitly creating the object that leads to its later implicit destruction.
To fix the problem, you just need to make sure that at the point where the code to destroy the std::unique_ptr<Widget::Impl>
is generated, Widget::Impl
is a complete type. The type becomes complete when its definition has been seen, and Widget::Impl
is defined inside widget.cpp
. The key to successful compilation, then, is to have the compiler see the body of Widget
âs destructor (i.e., the place where the compiler will generate code to destroy the std::unique_ptr
data member) only inside widget.cpp
after Widget::Impl
has been defined.
Arranging for that is simple. Declare Widget
âs destructor in widget.h
, but donât define it there:
class Widget { // as before, in "widget.h" public: Widget(); ~Widget(); // declaration only ⦠private: // as before struct Impl; std::unique_ptr<Impl> pImpl; };
Define it in widget.cpp
after Widget::Impl
has been defined:
#include "widget.h" // as before, in "widget.cpp" #include "gadget.h" #include <string> #include <vector> struct Widget::Impl { // as before, definition of std::string name; // Widget::Impl std::vector<double> data; Gadget g1, g2, g3; }; Widget::Widget() // as before : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() // ~Widget definition {}
This works well, and it requires the least typing, but if you want to emphasize that the compiler-generated destructor would do the right thingâthat the only reason you declared it was to cause its definition to be generated in Widget
âs implementation file, you can define the destructor body with â=
default
â:
Widget::~Widget() = default; // same effect as above
Classes using the Pimpl Idiom are natural candidates for move support, because compiler-generated move operations do exactly whatâs desired: perform a move on the underlying std::unique_ptr
. As Item 17 explains, the declaration of a destructor in Widget
prevents compilers from generating the move operations, so if you want move support, you must declare the functions yourself. Given that the compiler-generated versions would behave correctly, youâre likely to be tempted to implement them as follows:
class Widget { // still in public: // "widget.h" Widget(); ~Widget(); Widget(Widget&& rhs) = default; // right idea, Widget& operator=(Widget&& rhs) = default; // wrong code! ⦠private: // as before struct Impl; std::unique_ptr<Impl> pImpl; };
This approach leads to the same kind of problem as declaring the class without a destructor, and for the same fundamental reason. The compiler-generated move assignment operator needs to destroy the object pointed to by pImpl
before reassigning it, but in the Widget
header file, pImpl
points to an incomplete type. The situation is different for the move constructor. The problem there is that compilers typically generate code to destroy pImpl
in the event that an exception arises inside the move constructor, and destroying pImpl
requires that Impl
be complete.
Because the problem is the same as before, so is the fixâmove the definition of the move operations into the implementation file:
class Widget { // still in "widget.h" public: Widget(); ~Widget(); Widget(Widget&& rhs); // declarations Widget& operator=(Widget&& rhs); // only ⦠private: // as before struct Impl; std::unique_ptr<Impl> pImpl; }; #include <string> // as before, ⦠// in "widget.cpp" struct Widget::Impl { ⦠}; // as before Widget::Widget() // as before : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() = default; // as before Widget::Widget(Widget&& rhs) = default; // defini- Widget& Widget::operator=(Widget&& rhs) = default; // tions
The Pimpl Idiom is a way to reduce compilation dependencies between a classâs implementation and the classâs clients, but, conceptually, use of the idiom doesnât change what the class represents. The original Widget
class contained std::string
, std::vector
, and Gadget
data members, and, assuming that Gadget
s, like std::string
s and std::vector
s, can be copied, it would make sense for Widget
to support the copy operations. We have to write these functions ourselves, because (1) compilers wonât generate copy operations for classes with move-only types like std::unique_ptr
and (2) even if they did, the generated functions would copy only the std::unique_ptr
(i.e., perform a shallow copy), and we want to copy what the pointer points to (i.e., perform a deep copy).
In a ritual that is by now familiar, we declare the functions in the header file and implement them in the implementation file:
class Widget { // still in "widget.h" public: ⦠// other funcs, as before Widget(const Widget& rhs); // declarations Widget& operator=(const Widget& rhs); // only private: // as before struct Impl; std::unique_ptr<Impl> pImpl; }; #include "widget.h" // as before, ⦠// in "widget.cpp" struct Widget::Impl { ⦠}; // as before Widget::~Widget() = default; // other funcs, as before Widget::Widget(const Widget& rhs) // copy ctor : pImpl(nullptr) { if (rhs.pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); } Widget& Widget::operator=(const Widget& rhs) // copy operator= { if (!rhs.pImpl) pImpl.reset(); else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); else *pImpl = *rhs.pImpl; return *this; }
The implementations are straightforward, though we must handle cases where the parameter rhs
or, in the case of the copy assignment operator, *this
has been moved from and thus contains a null pImpl
pointer. In general, we take advantage of the fact that compilers will create the copy operations for Impl
, and these operations will copy each field automatically. We thus implement Widget
âs copy operations by calling Widget::Impl
âs compiler-generated copy operations. In both functions, note that we still follow the advice of Item 21 to prefer use of std::make_unique
over direct use of new
.
For purposes of implementing the Pimpl Idiom, std::unique_ptr
is the smart pointer to use, because the pImpl
pointer inside an object (e.g., inside a Widget
) has exclusive ownership of the corresponding implementation object (e.g., the Widget::Impl
object). Still, itâs interesting to note that if we were to use std::shared_ptr
instead of std::unique_ptr
for pImpl
(i.e., if the values in an Impl
struct could be shared by multiple Widget
s), weâd find that the advice of this Item no longer applied. Thereâd be no need to declare a destructor in Widget
, and without a user-declared destructor, compilers would happily generate the move operations, which would do exactly what weâd want them to. That is, given this code in widget.h
,
class Widget { // in "widget.h" public: Widget(); ⦠// no declarations for dtor // or move operations private: struct Impl; std::shared_ptr<Impl> pImpl; // std::shared_ptr }; // instead of std::unique_ptr
and this client code that #include
s widget.h
,
Widget w1; auto w2(std::move(w1)); // move-construct w2 w1 = std::move(w2); // move-assign w1
everything would compile and run as weâd hope: w1
would be default constructed, its value would be moved into w2
, that value would be moved back into w1
, and then both w1
and w2
would be destroyed (thus causing the pointed-to Widget::Impl
object to be destroyed).
The difference in behavior between std::unique_ptr
and std::shared_ptr
for pImpl
pointers stems from the differing ways these smart pointers support custom deleters. For std::unique_ptr
, the type of the deleter is part of the type of the smart pointer, and this makes it possible for compilers to generate smaller runtime data structures and faster runtime code. A consequence of this greater efficiency is that pointed-to types must be complete when compiler-generated special functions (e.g., destructors or move operations) are used. For std::shared_ptr
, the type of the deleter is not part of the type of the smart pointer. This necessitates larger runtime data structures and somewhat slower code, but pointed-to types need not be complete when compiler-generated special functions are employed.
For the Pimpl Idiom, thereâs not really a trade-off between the characteristics of std::unique_ptr
and std::shared_ptr
, because the relationship between classes like Widget
and classes like Widget::Impl
is exclusive ownership, and that makes std::unique_ptr
the proper tool for the job. Nevertheless, itâs worth knowing that in other situationsâsituations where shared ownership exists (and std::shared_ptr
is hence a fitting design choice), thereâs no need to jump through the function-definition hoops that use of std::unique_ptr
entails.
Things to Remember
-
The Pimpl Idiom decreases build times by reducing compilation dependencies between class clients and class implementations.
-
For
std::unique_ptr
pImpl
pointers, declare special member functions in the class header, but implement them in the implementation file. Do this even if the default function implementations are acceptable. -
The above advice applies to
std::unique_ptr
, but not tostd::shared_ptr
.
1 There are a few exceptions to this rule. Most stem from abnormal program termination. If an exception propagates out of a threadâs primary function (e.g., main
, for the programâs initial thread) or if a noexcept
specification is violated (see Item 14), local objects may not be destroyed, and if std::abort
or an exit function (i.e., std::_Exit
, std::exit
, or std::quick_exit
) is called, they definitely wonât be.
2 This implementation is not required by the Standard, but every Standard Library implementation Iâm familiar with employs it.
3 To create a full-featured make_unique
with the smallest effort possible, search for the standardization document that gave rise to it, then copy the implementation youâll find there. The document you want is N3656 by Stephan T. Lavavej, dated 2013-04-18.
4 In practice, the value of the weak count isnât always equal to the number of std::weak_ptr
s referring to the control block, because library implementers have found ways to slip additional information into the weak count that facilitate better code generation. For purposes of this Item, weâll ignore this and assume that the weak countâs value is the number of std::weak_ptr
s referring to the control block.
Get Effective Modern C++ now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.