// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
auto spv = std::make_shared>( initList);
For std::unique_ptr
, these two scenarios (custom deleters and braced initializers) are the only ones where its make
functions are problematic. For std::shared_ptr
and its make
functions, there are two more. Both are edge cases, but some developers live on the edge, and you may be one of them.
Some classes define their own versions of operator new
and operator delete
. The presence of these functions implies that the global memory allocation and deallocation routines for objects of these types are inappropriate. Often, class-specific routines are designed only to allocate and deallocate chunks of memory of precisely the size of objects of the class, e.g., operator new
and operator delete
for class Widget
are often designed only to handle allocation and deallocation of chunks of memory of exactly size sizeof(Widget)
. Such routines are a poor fit for std::shared_ptr
's support for custom allocation (via std::allocate_shared
) and deallocation (via custom deleters), because the amount of memory that std::allocate_shared
requests isn't the size of the dynamically allocated object, it's the size of that object plus the size of a control block. Consequently, using make
functions to create objects of types with class-specific versions of operator new
and operator delete
is typically a poor idea.
The size and speed advantages of std::make_shared
vis-a-vis direct use of new
stem from std::shared_ptr
's control block being placed in the same chunk of memory as the managed object. When that object's reference count goes to zero, the object is destroyed (i.e., its destructor is called). However, the memory it occupies can't be released until the control block has also been destroyed, because the same chunk of dynamically allocated memory contains both.
As I noted, the control block contains bookkeeping information beyond just the reference count itself. The reference count tracks how many std::shared_ptr
s refer to the control block, but the control block contains a second reference count, one that tallies how many std::weak_ptr
s refer to the control block. This second reference count is known as the weak count . [10] 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.
When a std::weak_ptr
checks to see if it has expired (see Item 19), it does so by examining the reference count (not the weak count) in the control block that it refers to. If the reference count is zero (i.e., if the pointed-to object has no std::shared_ptr
s referring to it and has thus been destroyed), the std::weak_ptr
has expired. Otherwise, it hasn't.
As long as std::weak_ptr
s refer to a control block (i.e., the weak count is greater than zero), that control block must continue to exist. And as long as a control block exists, the memory containing it must remain allocated. The memory allocated by a std::shared_ptr make
function, then, can't be deallocated until the last std::shared_ptr
and the last std::weak_ptr
referring to it have been destroyed.
If the object type is quite large and the time between destruction of the last std::shared_ptr
and the last std::weak_ptr
is significant, a lag can occur between when an object is destroyed and when the memory it occupied is freed:
class ReallyBigType { … };
auto pBigObj = // create very large
std::make_shared(); // object via
// std::make_shared
… // create std::shared_ptrs and std::weak_ptrs to
// large object, use them to work with it
… // final std::shared_ptr to object destroyed here,
// but std::weak_ptrs to it remain
…// during this period, memory formerly occupied
// by large object remains allocated
… // final std::weak_ptr to object destroyed here;
// memory for control block and object is released
With a direct use of new
, the memory for the ReallyBigType
object can be released as soon as the last std::shared_ptr
to it is destroyed:
class ReallyBigType { ... }; // as before
std::shared_ptr pBigObj( newReallyBigType);
// create very large
// object via new
… // as before, create std::shared_ptrs and
// std::weak_ptrs to object, use them with it
… // final std::shared_ptr to object destroyed here,
// but std::weak_ptrs to it remain;
// memory for object is deallocated
…// during this period, only memory for the
// control block remains allocated
… // final std::weak_ptr to object destroyed here;
// memory for control block is released
Should you find yourself in a situation where use of std::make_shared
is impossible or inappropriate, you'll want to guard yourself against the kind of exception-safety problems we saw earlier. The best way to do that is to make sure that when you use new
directly, you immediately pass the result to a smart pointer constructor in a statement that does nothing else . This prevents compilers from generating code that could emit an exception between the use of new
and invocation of the constructor for the smart pointer that will manage the new
ed object.
As an example, consider a minor revision to the exception-unsafe call to the processWidget
function we examined earlier. This time, we'll specify a custom deleter:
void processWidget(std::shared_ptr spw, // as before
int priority);
void cusDel(Widget *ptr); // custom
// deleter
Here's the exception-unsafe call:
processWidget( // as before,
std::shared_ptr(new Widget, cusDel), // potential
computePriority() // resource
); // leak!
Recall: if computePriority
is called after “ new Widget
” but before the std::shared_ptr
constructor, and if computePriority
yields an exception, the dynamically allocated Widget
will be leaked.
Here the use of a custom deleter precludes use of std::make_shared
, so the way to avoid the problem is to put the allocation of the Widget
and the construction of the std::shared_ptr
into their own statement, then call processWidget
with the resulting std::shared_ptr
. Here's the essence of the technique, though, as we'll see in a moment, we can tweak it to improve its performance:
Читать дальше