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
, and returns this new
::unique_ptrstd::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_ptr
s, and std::shared_ptr
s 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_ptr
s 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. aWidget
object must be created on the heap - The
std::shared_ptr
constructor responsible for managing thenew
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:
- Execute “
new Widget
“ - Execute
computePriority
- 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_ptr
s point to the control block, but the control block also has a second count that keeps track of how many std::weak_ptr
s 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_ptr
s 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, themake
function eliminates code duplication and improves exception safety. Forstd::make_shared
andstd::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_ptr
s, other cases where themake
function is not recommended include (1) classes with custom memory management (overloadedoperator new
andoperator delete
); (2) systems that pay special attention to memory, very large objects, andstd::weak_ptr
s than the correspondingstd: :shared_ptr
s live longer.