I l@ve RuBoard |
![]() ![]() |
1.9 Combining Policy ClassesThe 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 |
![]() ![]() |