Implementation of policies

In the previous section, we learned how to implement the simplest policy object. The policy can be of any type as long as it conforms to the interface convention, and is stored in the class as a data member. The policy object is most commonly generated by a template; however, it could be a regular, non-template, object that's specific to a particular pointer type, or even a function. The use of the policy was limited to a specific behavioral aspect, such as deletion of the object owned by the smart pointer.

There are several ways in which such policies can be implemented and used. First of all, let's review the declaration of a smart pointer with a deletion policy:

template <typename T, typename DeletionPolicy = DeleteByOperator<T>>
class SmartPtr { ..... };

Next, let's look at how we can construct a smart pointer object:

class C { ..... };
SmartPtr<C, DeleteByOperator<C>> p(new C, DeleteByOperator<C>());

One disadvantage of this design jumps out at once—the type C is mentioned four times in the definition of the object p—it must be consistent in all four places, or the code will not compile. C++17 allows us to simplify the definition somewhat:

SmartPtr p(new C, DeleteByOperator<C>());

Here, the constructor is used to deduce the parameters of the class template from the constructor arguments, in the manner similar to that of function templates. There are still two mentions of the type C that must be consistent.

One alternative implementation that works for stateless policies as well as for policy objects whose internal state does not depend on the types of the primary template (in our case, the type T of the SmartPtr template) is to make the policy itself a non-template object, but give it a template member function. For example, the DeleteByOperator policy is stateless (the object has no data members) and can be implemented without a class template:

struct DeleteByOperator {
template <typename T>
void operator()(T* p) const {
delete p;
}
};

This is a non-template object, so it does not need a type parameter. The member function template is instantiated on the type of object that needs to be deleted—the type is deduced by the compiler. Since the type of the policy object is always the same, we do not have to worry about specifying consistent types when creating the smart pointer object:

SmartPtr<C, DeleteByOperator> p(new C, DeleteByOperator()); // Before C++17
SmartPtr p(new C, DeleteByOperator()); // C++17

This object can be used by our smart pointer as it is, with no changes to the SmartPtr template, although we may want to change the default template argument:

template <typename T, typename DeletionPolicy = DeleteByOperator>
class SmartPtr { ..... };

A more complex policy, such as the heap deletion policy, can still be implemented using this approach:

struct DeleteHeap {
explicit DeleteHeap(SmallHeap& heap)
: heap_(heap) {}
template <typename T>
void operator()(T* p) const {
p->~T();
heap_.deallocate(p);
}
private:
Heap& heap_;
};

This policy has an internal state—the reference to the heap—but nothing in this policy object depends on the type T of the object we need to delete, except for the operator() member function. Therefore, the policy does not need to be parameterized by the object type. 

Since the main template, SmartPtr, did not have to be changed when we converted our policies from class templates to non-template classes with template member functions, there is no reason why we cannot use both types of policies with the same class. Indeed, any of the template class policies from the previous subsection would still work, so we can have some deletion policies implemented as classes and others as class templates. The latter is useful when the policy has data members whose type depends on the object type of the smart pointer.

If the policies are implemented as class templates, we have to specify the correct type to instantiate the policy for use with each specific policy-based class. In many cases, this is a very repetitive process—the same type is used to parameterize the main template and its policies. We can get the compiler to do this job for us if we use the entire template and not its particular instantiation as a policy:

template <typename T, template <typename> class DeletionPolicy = DeleteByOperator>
class SmartPtr {
public:
explicit SmartPtr(T* p = nullptr,
const DeletionPolicy<T>& deletion_policy = DeletionPolicy<T>()
) : p_(p),
deletion_policy_(deletion_policy)
{}
~SmartPtr() {
deletion_policy_(p_);
}
.....
};

Note the syntax for the second template parameter—template <typename> class DeletionPolicy. This is known as a template template parameter—the parameter of a template is itself a template. The class keyword is necessary in C++14 and earlier; in C++17, it can be replaced with typename. To use this parameter, we need to instantiate it with some type; in our case, it is the main template type parameter T. This ensures the consistency of the object type in the primary smart pointer template and its policies, although the constructor argument still must be constructed with the correct type:

SmartPtr<C, DeleteByOperator> p(new C, DeleteByOperator<C>());

Again, in C++17, the class template parameters can be deduced by the constructor; this works for template template parameters as well:

SmartPtr p(new C, DeleteByOperator<C>());

The template template parameters seem like an attractive alternative to the regular type parameters when the types are instantiated from a template anyway. Why don't we always use them? It turns out that a template template parameter has one significant limitation—the number of the template parameters has to match the specification precisely, including the default arguments. In other words, let's say I have the following template:

template <typename T, typename Heap = MyHeap> class DeleteHeap { ..... };

This template cannot be used as a parameter of the preceding smart pointer—it has two template parameters, while we only specified only one in the declaration of SmartPtr (a parameter with a default value is still a parameter). In contrast, we can use the instantiation of this template for a smart pointer that has a simple type, not a template, for the DeletionPolicy—we just need a class, and DeleteHeap<int, MyHeap> will do as good as any.

So far, we have always captured the policy object as a data member of the policy-based class. This approach to integrating classes into a larger class is known as composition. There are other ways in which the primary template can get access to the customized behavior algorithms provided by the policies, which we will consider next. 

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

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