• With each use of new, we use std::forwardto perfect-forward the arguments passed to makeInvestment(see Item 25). This makes all the information provided by callers available to the constructors of the objects being created.
• The custom deleter takes a parameter of type Investment*. Regardless of the actual type of object created inside makeInvestment(i.e., Stock, Bond, or RealEstate), it will ultimately be deleted inside the lambda expression as an Investment*object. This means we'll be deleting a derived class object via a base class pointer. For that to work, the base class — Investment— must have a virtual destructor:
class Investment {
public:
… // essential
virtual ~Investment();// design
… // component!
};
In C++14, the existence of function return type deduction (see Item 3) means that makeInvestmentcould be implemented in this simpler and more encapsulated fashion:
template
automakeInvestment(Ts&&... params) // C++14
{
auto delInvmt = [](Investment* pInvestment) // this is now
{ // inside
makeLogEntry(pInvestment); // make-
delete pInvestment; // Investment
};
std::unique_ptr // as
pInv(nullptr, delInvmt); // before
if ( … ) // as before
{
pInv.reset(new Stock(std::forward(params)...));
}
else if ( … ) // as before
{
pInv.reset(new Bond(std::forward(params)...));
}
else if ( … ) // as before
{
pInv.reset(new RealEstate(std::forward(params)...));
}
return pInv; // as before
}
I remarked earlier that, when using the default deleter (i.e., delete), you can reasonably assume that std::unique_ptrobjects are the same size as raw pointers. When custom deleters enter the picture, this may no longer be the case. Deleters that are function pointers generally cause the size of a std::unique_ptrto grow from one word to two. For deleters that are function objects, the change in size depends on how much state is stored in the function object. Stateless function objects (e.g., from lambda expressions with no captures) incur no size penalty, and this means that when a custom deleter can be implemented as either a function or a captureless lambda expression, the lambda is preferable:
auto delInvmt1 = [](Investment* pInvestment) // custom
{ // deleter
makeLogEntry(pInvestment); // as
delete pInvestment; // stateless
}; // lambda
template // return type
std::unique_ptr<Investment, decltype(delInvmt1)>// has size of
makeInvestment(Ts&&... args); // Investment*
void delInvmt2(Investment* pInvestment) // custom
{ // deleter
makeLogEntry(pInvestment); // as function
delete pInvestment;
}
template // return type has
std::unique_ptr<Investment, // size of Investment*
void (*)(Investment*)>// plus at least size
makeInvestment(Ts&&... params); // of function pointer!
Function object deleters with extensive state can yield std::unique_ptrobjects of significant size. If you find that a custom deleter makes your std::unique_ptrs unacceptably large, you probably need to change your design.
Factory functions are not the only common use case for std::unique_ptrs. They're even more popular as a mechanism for implementing the Pimpl Idiom. The code for that isn't complicated, but in some cases it's less than straightforward, so I'll refer you to Item 22, which is dedicated to the topic.
std::unique_ptrcomes in two forms, one for individual objects ( std::unique_ptr) and one for arrays ( std::unique_ptr). As a result, there's never any ambiguity about what kind of entity a std::unique_ptrpoints to. The std::unique_ptrAPI is designed to match the form you're using. For example, there's no indexing operator ( operator[]) for the single-object form, while the array form lacks dereferencing operators ( operator*and operator->).
The existence of std::unique_ptrfor arrays should be of only intellectual interest to you, because std::array, std::vector, and std::stringare virtually always better data structure choices than raw arrays. About the only situation I can conceive of when a std::unique_ptrwould make sense would be when you're using a C-like API that returns a raw pointer to a heap array that you assume ownership of.
std::unique_ptris the C++11 way to express exclusive ownership, but one of its most attractive features is that it easily and efficiently converts to a std::shared_ptr:
std::shared_ptrsp = // converts std::unique_ptr
makeInvestment( arguments ); // to std::shared_ptr
This is a key part of why std::unique_ptris so well suited as a factory function return type. Factory functions can't know whether callers will want to use exclusive-ownership semantics for the object they return or whether shared ownership (i.e., std::shared_ptr) would be more appropriate. By returning a std::unique_ptr, factories provide callers with the most efficient smart pointer, but they don't hinder callers from replacing it with its more flexible sibling. (For information about std::shared_ptr, proceed to Item 19.)
Things to Remember
• std::unique_ptris a small, fast, move-only smart pointer for managing resources with exclusive-ownership semantics.
• By default, resource destruction takes place via delete, but custom deleters can be specified. Stateful deleters and function pointers as deleters increase the size of std::unique_ptrobjects.
• Converting a std::unique_ptrto a std::shared_ptris easy.
Item 19: Use std::shared_ptrfor shared-ownership resource management.
Programmers using languages with garbage collection point and laugh at what C++ programmers go through to prevent resource leaks. “How primitive!” they jeer. “Didn't you get the memo from Lisp in the 1960s? Machines should manage resource lifetimes, not humans.” C++ developers roll their eyes. “You mean the memo where the only resource is memory and the timing of resource reclamation is nondeterministic? We prefer the generality and predictability of destructors, thank you.” But our bravado is part bluster. Garbage collection really is convenient, and manual lifetime management really can seem akin to constructing a mnemonic memory circuit using stone knives and bear skins. Why can't we have the best of both worlds: a system that works automatically (like garbage collection), yet applies to all resources and has predictable timing (like destructors)?
Читать дальше