Item 21: Prefer using std::make_unique and std::make_shared over direct use of new

Let’s lay the groundwork for std::make_unique and std::make_shared first. std::make_shared is part of the C++11 standard, but alas, std::make_unique is not. It joined the standard library starting with C++14. If you’re using C++11, don’t worry, a basic version of std::make_unique is easy to write yourself, as follows:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts & amp; & amp;... params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

As you can see, make_unique just perfectly forwards its parameters to the constructor of the object to be created, constructing std from the raw pointer generated by new ::unique_ptr, and returns this std::unique_ptr. This form of the function doesn’t support arrays and custom destructors (see Item 18), but it gives a demonstration that with a little effort you can write the make_unique function you want. (To implement a fully featured make_unique, go find the standardization file that provides this, and copy that implementation. The file you want is N3656, written by Stephan T. Lavavej in 2013- 04-18.) Remember, don’t put it in the std namespace, because you probably don’t want to see when you upgrade the C++14 standard library that you put The contents of the std namespace conflict with the contents of the std namespace provided by the compiler vendor.

std::make_unique and std::make_shared are two of the three make functions: accept any multi-parameter set, perfectly forward to The constructor dynamically allocates an object and returns a pointer to the object. The third make function is std::allocate_shared. It behaves the same as std::make_shared, except that the first parameter is an allocator object used to dynamically allocate memory.

Even a small comparison of creating smart pointers with and without the make function reveals the first reason why using the make function is better. For example:

auto upw1(std::make_unique<Widget>()); //Use the make function
std::unique_ptr<Widget> upw2(new Widget); //Do not use the make function
auto spw1(std::make_shared<Widget>()); //Use the make function
std::shared_ptr<Widget> spw2(new Widget); //Do not use make function

I’ve highlighted the key difference: the version using new duplicates the type, but the version of the make function does not. (The highlight here is Widget, and the declaration statement of new needs to write Widget twice, and the make function only It needs to be written once.) Rewrite types conflict with a key principle in software engineering: Duplicate code should be avoided. Duplication in source code increases compilation time, can lead to redundant object code, and generally makes codebase usage more difficult. It often morphs into inconsistent code, and inconsistencies in the code base often lead to bugs. Plus, typing twice is more taxing than once, and who doesn’t like typing less, right?

The second reason for using the make function has to do with exception safety. Suppose we have a function that handles Widget according to some priority:

void processWidget(std::shared_ptr<Widget> spw, int priority);

Passing a std::shared_ptr by value might seem suspicious, but Item 41 explains that if processWidget always copies a std::shared_ptr (eg , by storing it in a data structure of the processed Widget), then this may be a reasonable design choice.

Now suppose we have a function to calculate relative priority,

int computePriority();

And we used new instead of std::make_shared when calling processWidget:

processWidget(std::shared_ptr<Widget>(new Widget), //Potential resource leak!
              computePriority());

As the comments say, this code may leak when new a Widget. why? Both the calling code and the called function use std::shared_ptrs, and std::shared_ptrs are designed to prevent leaks. They automatically free the memory they point to when the last std::shared_ptr is destroyed. How can this code leak if everyone is using std::shared_ptrs everywhere?

The answer has to do with how the compiler converts source code into object code. At runtime, the actual parameters of a function must be calculated before the function is called, so before calling processWidget, the following operations must be performed, and processWidget will start to execute:

  • The expression “new Widget” must evaluate, i.e. a Widget object must be created on the heap
  • The std::shared_ptr constructor responsible for managing the new pointer must be executed
  • computePriority must be running

The compiler does not need to generate code in the order of execution. “new Widget” must be executed before the constructor of std::shared_ptr is called, because the result of new is used as the actual parameter of the constructor , but computePriority may be executed before, after, or between. That is, the compiler may generate code in this order of execution:

  1. Execute “new Widget
  2. Execute computePriority
  3. Run the std::shared_ptr constructor

If the code is generated like this, and computePriority generates an exception at runtime, then the Widget dynamically allocated in the first step will leak. Because it will never be managed by std::shared_ptr in the third step.

Use std::make_shared to prevent this problem. The calling code looks like this:

processWidget(std::make_shared<Widget>(), //No potential resource leaks
              computePriority());

At runtime, one of std::make_shared and computePriority will be called first. If std::make_shared is called first, before computePriority is called, the raw pointer of the dynamically allocated Widget will be safely stored as the return value std::shared_ptr. If the computePriority generates an exception, the std::shared_ptr destructor will ensure that the managed Widget is destroyed. If computePriority is called first and generates an exception, then std::make_shared will not be called, so there is no need to worry about dynamically allocating Widget (will leak).

If we replace std::shared_ptr, std::make_shared with std::unique_ptr, std::make_unique, the same reasoning applies. Therefore, when writing exception-safe code, using std::make_unique instead of new is the same as using std::make_shared (rather than new) are equally important.

One feature of std::make_shared (compared to using new directly) is the efficiency gain. Using std::make_shared allows the compiler to generate smaller, faster code, and use cleaner data structures. Consider the following direct use of new:

std::shared_ptr<Widget> spw(new Widget);

Obviously, this code needs to do a memory allocation, but it actually does it twice. Item 19 explains that each std::shared_ptr points to a control block that contains the reference count of the pointed-to object, among other things. The memory for this control block is allocated in the std::shared_ptr constructor. Therefore, using new directly requires a memory allocation for the Widget and another memory allocation for the control block.

If you use std::make_shared instead:

auto spw = std::make_shared<Widget>();

One allocation is enough. This is because std::make_shared allocates a block of memory, which simultaneously accommodates 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 executable code because the memory is allocated only once. Additionally, using std::make_shared avoids the need for some bookkeeping information in control blocks, potentially reducing the program’s overall memory footprint.

The efficiency analysis of std::make_shared is also applicable to std::allocate_shared, so the performance advantage of std::make_shared is also extended to this function .

The debate over using the make function in favor of using new directly is fierce. Despite their advantages in terms of software engineering, exception safety, and efficiency, the recommendation of this Item is to prefer to use the make functions rather than relying entirely on them. This is because there are situations where they cannot or should not be used.

For example, neither make function allows specifying a custom deleter (see Items 18 and 19), but std::unique_ptr and std::shared_ptr There are constructors that do this. There is a custom deleter for Widget:

auto widgetDeleter = [](Widget* pw) { ... };

Creating a smart pointer that uses it can only be done directly using new:

std::unique_ptr<Widget, decltype(widgetDeleter)>
    upw(new Widget, widgetDeleter);

std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

There is no way to do the same for the make function.

The second limitation of the make function comes from syntax details in its implementation. Item 7 explains that when a constructor is overloaded, with an overload that takes std::initializer_list as a parameter and one that doesn’t, objects created with curly braces prefer The overloaded form that uses std::initializer_list as a parameter, and creating an object with parentheses will call the overloaded form that does not use std::initializer_list as a parameter. The make functions forward their arguments perfectly to the object constructor, but do they use parentheses or curly braces? For some types, the answers to the questions will be very different. For example, in these calls,

auto upv = std::make_unique<std::vector<int>>(10, 20);
auto spv = std::make_shared<std::vector<int>>(10, 20);

The good news is that this is not undefined: both calls create a std::vector of 10 elements, each with a value of 20. This means that in the make function, perfect forwarding uses parentheses, not curly braces. The bad news is that if you want to initialize the pointed-to object with curly braces, you have to use new directly. Using the make function would require the ability to perfectly forward brace initialization, but, as Item 30 says, brace initialization cannot be perfectly forward. However, Item 30 introduces a workaround: use auto type deduction to create a std::initializer_list object from the curly-brace initialization (see Item 2), then pass the The object created by auto is passed to the make function.

//Create std::initializer_list
auto initList = { 10, 20 };
//Use std::initializer_list to create std::vector for the parameter constructor
auto spv = std::make_shared<std::vector<int>>(initList);

For std::unique_ptr, only these two scenarios (custom deleter and brace initialization) are somewhat problematic using the make function. For std::shared_ptr and its make function, there are 2 more problems. These are edge cases, but they happen to some developers, and you may be one of them.

Some classes overload operator new and operator delete. The existence of these functions means that global memory allocation and deallocation for objects of these types is irregular. Designing this kind of custom operation often only allocates and frees the memory of the object size accurately. For example, the operator new and operator delete of the Widget class will only handle memory blocks of sizeof(Widget) Allocate and free. This set of behaviors is less applicable to std::shared_ptr support for custom allocation (via std::allocate_shared) and deallocation (via custom deleter), because The total memory size required by std::allocate_shared is not equal to the size of the dynamically allocated object, and adds the size of the control block. Therefore, using the make function to create objects of classes that overload operator new and operator delete is typically a bad idea.

Compared with using new directly, the advantages of std::make_shared in size and speed come from the control block and pointer to std::shared_ptr objects in the same block of memory. When the object’s reference count drops to 0, the object is destroyed (ie the destructor is called). However, because the control block and the object are placed in the same allocated memory block, the memory occupied by the object is not released until the control block’s memory is also destroyed.

As I said, control blocks contain bookkeeping information in addition to reference counting. The reference count keeps track of how many std::shared_ptrs point to the control block, but the control block also has a second count that keeps track of how many std::weak_ptrs point to the control block. The second reference count is weak count. (Actually, the value of weak count is not always equal to the number of std::weak_ptr pointing to the control block, because the implementor of the library finds some methods in weak count to facilitate better code generation. For the purpose of this item, we will ignore this and assume that the value of weak count is equal to the std pointing to the control block ::weak_ptr.) When a std::weak_ptr checks whether it is expired (see Item 19), it checks the reference count in the control block it points to (instead of the < em>weak count). If the reference count is 0 (that is, the object has no std::shared_ptr pointing to it again, it has been destroyed), std::weak_ptr has expired. Otherwise it will not expire.

As long as std::weak_ptrs refer to a control block (i.e. weak count is greater than zero), the control block must continue to exist. As long as the control block exists, the memory containing it must remain allocated. Memory allocated by the make function of std::shared_ptr until the last std::shared_ptr and the last std pointing to it ::weak_ptr has been destroyed before being released.

If the object type is very large, and the time between destroying the last std::shared_ptr and destroying the last std::weak_ptr is long, then between destroying the object and freeing it There may be a delay between memory usage.

class ReallyBigType { ... };

auto pBigObj = // via std::make_shared
    std::make_shared<ReallyBigType>(); //Create a large object
                    
... // create std::shared_ptrs and std::weak_ptrs
            // point to this object, use them

... //The last std::shared_ptr is destroyed here,
            //but std::weak_ptrs are still there

... //At this stage, the memory originally allocated to the large object is still allocated

... //the last std::weak_ptr is destroyed here;
            //The memory of the control block and object is released

Just use new directly, once the last std::shared_ptr is destroyed, the memory of the ReallyBigType object will be released:

class ReallyBigType { … }; //Same as before

std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
                                        //Create a large object through new

... // Create std::shared_ptrs and std::weak_ptrs as before
            // point to this object, use them
            
... //The last std::shared_ptr is destroyed here,
            //But std::weak_ptrs are still there;
            // object's memory is freed

... // At this stage, only the control block's memory remains allocated

... //the last std::weak_ptr is destroyed here;
            //control block memory is freed

If you find yourself in a situation where using std::make_shared is not possible or appropriate, you will want to insure yourself against the exception safety issues we saw earlier. The best way is to make sure that when using new directly, in a statement that does nothing else, the result is immediately passed to the smart pointer constructor. This prevents compiler-generated code from throwing exceptions between using new and calling the constructor that manages the smart pointer of the object that new produces.

For example, consider the processWidget function we discussed earlier, with a small modification of its non-exception-safe calls. This time, we’ll specify a custom deleter:

void processWidget(std::shared_ptr<Widget> spw, //same as before
                   int priority);
void cusDel(Widget *ptr); //custom deleter

Here’s the non-exception-safe call:

processWidget( //Same as before,
    std::shared_ptr<Widget>(new Widget, cusDel), //potential memory leak!
    computePriority()
);

Recall: if computePriority is called after “new Widget” but before std::shared_ptr constructor, and if computePriority generates an exception, then the dynamically allocated Widget will leak.

The use of custom delete here excludes the use of std::make_shared, so the way to avoid problems is to combine the allocation of Widget with std::shared_ptr code>’s constructs into their own statement, and then use the resulting std::shared_ptr to call processWidget. This is the essence of the technique, however, as we’ll see later, we can tweak it to improve its performance:

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); // correct, but not optimized, see below

This works because std::shared_ptr takes ownership of the raw pointer passed to its constructor, even if the constructor raises an exception. In this example, if the constructor of spw throws an exception (such as unable to dynamically allocate memory for the control block), it is still guaranteed that cusDel will be in “new Widget” on the pointer generated.

A small performance issue is that we pass an rvalue to processWidget in the non-exception-safe call:

processWidget(
    std::shared_ptr<Widget>(new Widget, cusDel), //The actual parameter is an rvalue
    computePriority()
);

But in exception-safe calls, we pass lvalues:

processWidget(spw, computePriority()); //The actual parameter is an lvalue

Because the std::shared_ptr formal parameter of processWidget is passed by value, constructing from an rvalue only needs to be moved, while constructing from an lvalue needs to be copied. For std::shared_ptr, this distinction is meaningful, because copying std::shared_ptr requires an atomic increment of the reference count, while moving does not require reference counting There is operation. To bring exception-safe code to the performance level of non-exception-safe code, we need to use std::move to convert spw to an rvalue (see Item 23):

processWidget(std::move(spw), computePriority()); //efficient and exception-safe

This is interesting and worth knowing, but usually irrelevant since you have very few reasons not to use the make function. Unless you have a compelling reason to do so, you should use the make function.

Remember:

  • Compared with using new directly, the make function eliminates code duplication and improves exception safety. For std::make_shared and std::allocate_shared, the generated code is smaller and faster.
  • Situations where the make function is not suitable include specifying a custom deleter and wishing to initialize with curly braces.
  • For std::shared_ptrs, other cases where the make function is not recommended include (1) classes with custom memory management (overloaded operator new and operator delete); (2) systems that pay special attention to memory, very large objects, and std::weak_ptrs than the corresponding std: :shared_ptrs live longer.