Now suppose we have a function to compute the relevant priority,
int computePriority();
and we use that in a call to processWidgetthat uses newinstead of std::make_shared:
processWidget(std::shared_ptr(new Widget), // potential
computePriority()); // resource
// leak!
As the comment indicates, this code could leak the Widgetconjured up by new. But how? Both the calling code and the called function are using std::shared_ptrs, and std::shared_ptrs are designed to prevent resource leaks. They automatically destroy what they point to when the last std::shared_ptrpointing there goes away. If everybody is using std::shared_ptrs 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 processWidgetcan begin execution:
• The expression “ new Widget” must be evaluated, i.e., a Widgetmust be created on the heap.
• The constructor for the std::shared_ptrresponsible for managing the pointer produced by newmust be executed.
• computePrioritymust run.
Compilers are not required to generate code that executes them in this order. “ new Widget” must be executed before the std::shared_ptrconstructor may be called, because the result of that newis used as an argument to that constructor, but computePrioritymay 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_ptrconstructor.
If such code is generated and, at runtime, computePriorityproduces an exception, the dynamically allocated Widgetfrom Step 1 will be leaked, because it will never be stored in the std::shared_ptrthat's supposed to start managing it in Step 3.
Using std::make_sharedavoids this problem. Calling code would look like this:
processWidget( std::make_shared(), // no potential
computePriority()); // resource leak
At runtime, either std::make_sharedor computePrioritywill be called first. If it's std::make_shared, the raw pointer to the dynamically allocated Widgetis safely stored in the returned std::shared_ptrbefore computePriorityis called. If computePrioritythen yields an exception, the std::shared_ptrdestructor will see to it that the Widgetit owns is destroyed. And if computePriorityis called first and yields an exception, std::make_sharedwill not be invoked, and there will hence be no dynamically allocated Widgetto worry about.
If we replace std::shared_ptrand std::make_sharedwith std::unique_ptrand std::make_unique, exactly the same reasoning applies. Using std::make_uniqueinstead of newis 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_sharedallows 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_ptrpoints 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_ptrconstructor. Direct use of new, then, requires one memory allocation for the Widgetand a second allocation for the control block.
If std::make_sharedis used instead,
auto spw = std::make_shared();
one allocation suffices. That's because std::make_sharedallocates a single chunk of memory to hold both the Widgetobject 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_sharedobviates 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_sharedis equally applicable to std::allocate_shared, so the performance advantages of s td::make_sharedextend to that function, as well.
The arguments for preferring makefunctions over direct use of neware strong ones. Despite their software engineering, exception safety, and efficiency advantages, however, this Item's guidance is to prefer the makefunctions, 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 makefunctions permit the specification of custom deleters (see Items 18and 19), but both std::unique_ptrand std::shared_ptrhave 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 makefunction.
A second limitation of makefunctions 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_listparameters, creating the object using braces prefers the std::initializer_listconstructor, while creating the object using parentheses calls the non- std::initializer_listconstructor. The makefunctions 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::vectors with 10 elements, each of value 20, or to std::vectors 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::vectors of size 10 with all values set to 20. That means that within the makefunctions, 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 newdirectly. Using a makefunction 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 autotype deduction to create a std::initializer_listobject from a braced initializer (see Item 2), then pass the auto-created object through the makefunction:
Читать дальше