16.1.6. Efficiency and Flexibility

Image

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.


Exercises Section 16.1.5

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.

Binding the Deleter at Run Time

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 function14.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.

Binding the Deleter at Compile Time

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.


Exercises Section 16.1.6

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.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.19.243