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