I l@ve RuBoard Previous Section Next Section

1.9 Combining Policy Classes

The greatest usefulness of policies is apparent when you combine them. Typically, a highly configurable class uses several policies for various aspects of its workings. Then the library user selects the desired high-level behavior by combining several policy classes.

For example, consider designing a generic smart pointer class. (Chapter 7 builds a full implementation.) Say you identify two design choices that you should establish with policies: threading model and checking before dereferencing. Then you implement a SmartPtr class template that uses two policies, as shown:



template


<


   class T,


   template <class> class CheckingPolicy,


   template <class> class ThreadingModel


>


class SmartPtr;


SmartPtr has three template parameters: the pointee type and two policies. Inside SmartPtr, you orchestrate the two policies into a sound implementation. SmartPtr be comes a coherent shell that integrates several policies, rather than a rigid, canned implementation. By designing SmartPtr this way, you allow the user to configure SmartPtr with a simple typedef:



typedef SmartPtr<Widget, NoChecking, SingleThreaded>


   WidgetPtr;


Inside the same application, you can define and use several smart pointer classes:



typedef SmartPtr<Widget, EnforceNotNull, SingleThreaded>


   SafeWidgetPtr;


The two policies can be defined as follows:

Checking: The CheckingPolicy<T> class template must expose a Check member function, callable with an lvalue of type T*. SmartPtr calls Check, passing it the pointee object before dereferencing it.

ThreadingModel: The ThreadingModel<T> class template must expose an inner type called Lock, whose constructor accepts a T&. For the lifetime of a Lock object, operations on the T object are serialized.

For example, here is the implementation of the NoChecking and EnforceNotNull policy classes:



template <class T> struct NoChecking


{


   static void Check(T*) {}


};


template <class T> struct EnforceNotNull


{


   class NullPointerException : public std::exception { ... };


   static void Check(T* ptr)


   {


      if (!ptr) throw NullPointerException();


   }


};


By plugging in various checking policy classes, you can implement various behaviors. You can even initialize the pointee object with a default value by accepting a reference to a pointer, as shown:



template <class T> struct EnsureNotNull


{


   static void Check(T*& ptr)


   {


      if (!ptr) ptr = GetDefaultValue();


   }


};


SmartPtr uses the Checking policy this way:



template


<


   class T,


   template <class> class CheckingPolicy,


   template <class> class ThreadingModel


>


class SmartPtr


   : public CheckingPolicy<T>


   , public ThreadingModel<SmartPtr>


{


   ...


   T* operator->()


   {


      typename ThreadingModel<SmartPtr>::Lock guard(*this);


      CheckingPolicy<T>::Check(pointee_);


      return pointee_;


   }


private:


   T* pointee_;


};


Notice the use of both the CheckingPolicy and ThreadingModel policy classes in the same function. Depending on the two template arguments, SmartPtr::operator-> behaves differently on two orthogonal dimensions. Such is the power of combining policies.

If you manage to decompose a class in orthogonal policies, you can cover a large spectrum of behaviors with a small amount of code.

    I l@ve RuBoard Previous Section Next Section