Now suppose we have a function to compute the relevant priority,
int computePriority();
and we use that in a call to processWidget
that uses new
instead of std::make_shared
:
processWidget(std::shared_ptr(new Widget), // potential
computePriority()); // resource
// leak!
As the comment indicates, this code could leak the Widget
conjured up by new
. But how? Both the calling code and the called function are using std::shared_ptr
s, and std::shared_ptr
s are designed to prevent resource leaks. They automatically destroy what they point to when the last std::shared_ptr
pointing there goes away. If everybody is using std::shared_ptr
s everywhere, how can this code leak?
The answer has to do with compilers' translation of source code into object code. At runtime, the arguments for a function must be evaluated before the function can be invoked, so in the call to processWidget
, the following things must occur before processWidget
can begin execution:
• The expression “ new Widget
” must be evaluated, i.e., a Widget
must be created on the heap.
• The constructor for the std::shared_ptr
responsible for managing the pointer produced by new
must be executed.
• computePriority
must run.
Compilers are not required to generate code that executes them in this order. “ new Widget
” must be executed before the std::shared_ptr
constructor may be called, because the result of that new
is used as an argument to that constructor, but computePriority
may be executed before those calls, after them, or, crucially, between them. That is, compilers may emit code to execute the operations in this order:
• Perform “ new Widget
”.
• Execute computePriority
.
• Run std::shared_ptr
constructor.
If such code is generated and, at runtime, computePriority
produces an exception, the dynamically allocated Widget
from Step 1 will be leaked, because it will never be stored in the std::shared_ptr
that's supposed to start managing it in Step 3.
Using std::make_shared
avoids this problem. Calling code would look like this:
processWidget( std::make_shared(), // no potential
computePriority()); // resource leak
At runtime, either std::make_shared
or computePriority
will be called first. If it's std::make_shared
, the raw pointer to the dynamically allocated Widget
is safely stored in the returned std::shared_ptr
before computePriority
is called. If computePriority
then yields an exception, the std::shared_ptr
destructor will see to it that the Widget
it owns is destroyed. And if computePriority
is called first and yields an exception, std::make_shared
will not be invoked, and there will hence be no dynamically allocated Widget
to worry about.
If we replace std::shared_ptr
and std::make_shared
with std::unique_ptr
and std::make_unique
, exactly the same reasoning applies. Using std::make_unique
instead of new
is thus just as important in writing exception-safe code as using std::make_shared
.
A special feature of std::make_shared
(compared to direct use of new
) is improved efficiency. Using std::make_shared
allows compilers to generate smaller, faster code that employs leaner data structures. Consider the following direct use of new
:
std::shared_ptr spw(new Widget);
It's obvious that this code entails a memory allocation, but it actually performs two. Item 19explains that every std::shared_ptr
points to a control block containing, among other things, the reference count for the pointed-to object. Memory for this control block is allocated in the std::shared_ptr
constructor. Direct use of new
, then, requires one memory allocation for the Widget
and a second allocation for the control block.
If std::make_shared
is used instead,
auto spw = std::make_shared();
one allocation suffices. That's because std::make_shared
allocates a single chunk of memory to hold both the Widget
object and the control block. This optimization reduces the static size of the program, because the code contains only one memory allocation call, and it increases the speed of the executable code, because memory is allocated only once. Furthermore, using std::make_shared
obviates the need for some of the bookkeeping information in the control block, potentially reducing the total memory footprint for the program.
The efficiency analysis for std::make_shared
is equally applicable to std::allocate_shared
, so the performance advantages of s td::make_shared
extend to that function, as well.
The arguments for preferring make
functions over direct use of new
are strong ones. Despite their software engineering, exception safety, and efficiency advantages, however, this Item's guidance is to prefer the make
functions, not to rely on them exclusively. That's because there are circumstances where they can't or shouldn't be used.
For example, none of the make
functions permit the specification of custom deleters (see Items 18and 19), but both std::unique_ptr
and std::shared_ptr
have constructors that do. Given a custom deleter for a Widget
,
auto widgetDeleter = [](Widget* pw) { … };
creating a smart pointer using it is straightforward using new
:
std::unique_ptr
upw( new Widget, widgetDeleter);
std::shared_ptr spw( new Widget, widgetDeleter);
There's no way to do the same thing with a make
function.
A second limitation of make
functions stems from a syntactic detail of their implementations. Item 7explains that when creating an object whose type overloads constructors both with and without std::initializer_list
parameters, creating the object using braces prefers the std::initializer_list
constructor, while creating the object using parentheses calls the non- std::initializer_list
constructor. The make
functions perfect-forward their parameters to an object's constructor, but do they do so using parentheses or using braces? For some types, the answer to this question makes a big difference. For example, in these calls,
auto upv = std::make_unique>(10, 20);
auto spv = std::make_shared>(10, 20);
do the resulting smart pointers point to std::vector
s with 10 elements, each of value 20, or to std::vector
s with two elements, one with value 10 and the other with value 20? Or is the result indeterminate?
The good news is that it's not indeterminate: both calls create std::vector
s of size 10 with all values set to 20. That means that within the make
functions, the perfect forwarding code uses parentheses, not braces. The bad news is that if you want to construct your pointed-to object using a braced initializer, you must use new
directly. Using a make
function would require the ability to perfect-forward a braced initializer, but, as Item 30explains, braced initializers can't be perfect-forwarded. However, Item 30also describes a workaround: use auto
type deduction to create a std::initializer_list
object from a braced initializer (see Item 2), then pass the auto
-created object through the make
function:
Читать дальше