The library smart pointer types (§ 12.1, p. 450) offer a good illustration of design choices faced by designers of templates.
The obvious difference between shared_ptr
and unique_ptr
is the strategy they use in managing the pointer they hold—one class gives us shared ownership; the other owns the pointer that it holds. This difference is essential to what these classes do.
These classes also differ in how they let users override their default deleter. We can easily override the deleter of a shared_ptr
by passing a callable object when we create or reset
the pointer. In contrast, the type of the deleter is part of the type of a unique_ptr
object. Users must supply that type as an explicit template argument when they define a unique_ptr
. As a result, it is more complicated for users of unique_ptr
to provide their own deleter.
Exercise 16.25: Explain the meaning of these declarations:
extern template class vector<string>;
template class vector<Sales_data>;
Exercise 16.26: Assuming NoDefault
is a class that does not have a default constructor, can we explicitly instantiate vector<NoDefault>
? If not, why not?
Exercise 16.27: For each labeled statement explain what, if any, instantiations happen. If a template is instantiated, explain why; if not, explain why not.
template <typename T> class Stack { };
void f1(Stack<char>); // (a)
class Exercise {
Stack<double> &rsd; // (b)
Stack<int> si; // (c)
};
int main() {
Stack<char> *sc; // (d)
f1(*sc); // (e)
int iObj = sizeof(Stack< string >); // (f)
}
The difference in how the deleter is handled is incidental to the functionality of these classes. However, as we’ll see, this difference in implementation strategy may have important performance impacts.
Although we don’t know how the library types are implemented, we can infer that shared_ptr
must access its deleter indirectly. That is the deleter must be stored as a pointer or as a class (such as function
(§ 14.8.3, p. 577)) that encapsulates a pointer.
We can be certain that shared_ptr
does not hold the deleter as a direct member, because the type of the deleter isn’t known until run time. Indeed, we can change the type of the deleter in a given shared_ptr
during that shared_ptr
’s lifetime. We can construct a shared_ptr
using a deleter of one type, and subsequently use reset
to give that same shared_ptr
a different type of deleter. In general, we cannot have a member whose type changes at run time. Hence, the deleter must be stored indirectly.
To think about how the deleter must work, let’s assume that shared_ptr
stores the pointer it manages in a member named p
, and that the deleter is accessed through a member named del
. The shared_ptr
destructor must include a statement such as
// value of del known only at run time; call through a pointer
del ? del(p) : delete p; // del(p) requires run-time jump to del's location
Because the deleter is stored indirectly, the call del(p)
requires a run-time jump to the location stored in del
to execute the code to which del
points.
Now, let’s think about how unique_ptr
might work. In this class, the type of the deleter is part of the type of the unique_ptr
. That is, unique_ptr
has two template parameters, one that represents the pointer that the unique_ptr
manages and the other that represents the type of the deleter. Because the type of the deleter is part of the type of a unique_ptr
, the type of the deleter member is known at compile time. The deleter can be stored directly in each unique_ptr
object.
The unique_ptr
destructor operates similarly to its shared_ptr
counterpart in that it calls a user-supplied deleter or executes delete
on its stored pointer:
// del bound at compile time; direct call to the deleter is instantiated
del(p); // no run-time overhead
The type of del
is either the default deleter type or a user-supplied type. It doesn’t matter; either way the code that will be executed is known at compile time. Indeed, if the deleter is something like our DebugDelete
class (§ 16.1.4, p. 672) this call might even be inlined at compile time.
By binding the deleter at compile time, unique_ptr
avoids the run-time cost of an indirect call to its deleter. By binding the deleter at run time, shared_ptr
makes it easier for users to override the deleter.
Exercise 16.28: Write your own versions of shared_ptr
and unique_ptr
.
Exercise 16.29: Revise your Blob
class to use your version of shared_ptr
rather than the library version.
Exercise 16.30: Rerun some of your programs to verify your shared_ptr
and revised Blob
classes. (Note: Implementing the weak_ptr
type is beyond the scope of this Primer, so you will not be able to use the BlobPtr
class with your revised Blob
.)
Exercise 16.31: Explain how the compiler might inline the call to the deleter if we used DebugDelete
with unique_ptr
.