ch1.policy_based_class_design
Ch 1. Policy-based Class Design
-
Core Concept: Policy-based class design is a key technique for creating flexible and highly reusable libraries. It involves building a class with complex behavior using numerous small classes, known as policies, where each policy addresses a specific behavioral or structural aspect.
-
Policy Interface: Each policy defines an interface related to a specific issue. Various implementations of these policies are possible as long as they adhere to the established policy interface.
-
Combination of Policies: The strength of this design lies in its ability to mix and match policies, allowing for a wide range of behaviors from a relatively small set of basic components.
-
Practical Applications: The concept of policies is applied in several chapters of the book:
-
Chapter 6's
SingletonHolderclass template utilizes policies for lifetime management and thread safety. -
Chapter 7's
SmartPtris predominantly constructed from policies. -
In Chapter 11, the double-dispatch engine employs policies to choose among different trade-offs.
-
The generic Abstract Factory implementation in Chapter 9 adopts a policy for selecting a creation method.
1.1 The Multiplicity of Software Design
-
Multiplicity in Software Engineering: Unlike many other engineering fields, software engineering is characterized by a vast array of correct approaches to any given problem. This richness in diversity means that every solution chosen opens up a myriad of other possibilities, creating a combinatorial landscape of design options. This extends from high-level system architecture to the minutest coding details.
-
Example of Smart Pointer Design: To illustrate this concept, consider the design of a smart pointer:
- It can be either single-threaded or multithreaded.
- It might implement different ownership strategies.
- It may balance between safety and speed in various ways.
- It could either support or not support automatic conversions to raw pointer types.
- The optimal combination of these features typically depends on the specific needs of different parts of an application.
-
Challenges for Novice Designers: For those new to software design, this multiplicity can be bewildering. Faced with a design problem, numerous solutions like events, objects, observers, callbacks, virtuals, and templates might all seem viable. However, discerning which is the best fit is often challenging.
-
Expertise in Software Architecture:
- The key difference between an experienced software architect and a novice is the ability to understand what works and what doesn't in various scenarios. Experienced architects can foresee how different solutions might scale and are aware of their unique pros and cons. They recognize that a solution that seems viable in theory might not work well in practice.
- Experienced designers, akin to skilled chess players, can anticipate the consequences of their design choices further ahead. While programming talent might emerge early in one's career, the mastery of software design often requires more time to develop.
-
Library Design Challenges: For library developers, the combinatorial nature of design poses significant challenges. They must account for a wide range of typical use cases while keeping the library flexible for specific needs. Questions arise about how to package versatile design components, configure them for user needs, and manage the vast diversity of design with a manageable amount of code.
-
Objective of the Chapter and Book: The chapter, and the book as a whole, aim to address these challenges. They seek to provide answers on how to navigate the complex landscape of software design, offering solutions for packaging flexible design components and managing the inherent multiplicity in software engineering.
1.2 The Failure of the Do-It-All Interface
-
Problems with Do-It-All Interfaces:
- Intellectual Overhead: Complex interfaces create a steep learning curve.
- Size and Inefficiency: Large, all-encompassing classes lead to bloated and slow code.
- Loss of Static Type Safety: Overly rich interfaces can compromise type safety, an essential aspect of many programming languages.
-
System Architecture and Enforcing Axioms:
- A well-designed system should enforce constraints naturally.
- For instance, preventing the creation of multiple Singleton objects or objects from disjoint families.
- Constraints should ideally be enforced at compile time, which becomes challenging with large, all-encompassing interfaces.
-
Semantic vs. Syntactic Validity:
- A gap often exists between what is syntactically valid (what the language allows) and semantically valid (what makes sense in the application context). Overly broad interfaces exacerbate this issue.
-
Example of Singleton and Threading:
- If a library completely abstracts threading, it may not be usable with specific nonportable threading systems.
- Exposing low-level functions risks breaking the design through syntactically correct but semantically incorrect code.
-
Canned Design Solutions and Combinatorial Explosion:
- Implementing different designs as separate classes (like various types of smart pointers) can lead to an overwhelming number of combinations.
- This approach is impractical and rigid, as it cannot easily adapt to unforeseen requirements.
-
Library Rigidity and Intellectual Overhead:
- Highly specific library solutions fail to accommodate slight customization needs, rendering them ineffective for slightly different use cases.
-
Libraries Should Assist Rather Than Dictate Design:
- Libraries targeting design should facilitate the enforcement of constraints in user-crafted designs, rather than imposing predefined constraints.
- Flexibility is key; while providing popular or recommended solutions is helpful, the ability for programmers to modify these solutions is crucial.
-
State of the Art in Library Space:
- There's an abundance of low-level general-purpose and specialized libraries, but a lack of libraries that assist in higher-level application design.
- This gap is paradoxical, considering most applications require some level of design.
-
Frameworks vs. Design Flexibility:
- Frameworks often lock applications into specific designs, rather than aiding in the customization and choice of a design.
- For original designs, programmers frequently need to start from basic constructs like classes and functions.
1.3 Multiple Inheritance to the Rescue?
-
Multiple Inheritance for Combining Features:
- The concept of a
TemporarySecretaryclass, inheriting from bothSecretaryandTemporaryclasses, introduces the idea of using multiple inheritance to handle a diverse set of design choices through a few well-chosen base classes. - For instance, to create a multithreaded, reference-counted smart pointer, one might consider inheriting from
BaseSmartPtr,MultiThreaded, andRefCounted.
- The concept of a
-
Limitations of Naïve Multiple Inheritance Design:
- Experienced class designers recognize that this straightforward approach to multiple inheritance is often ineffective.
- The analysis of its shortcomings offers insights into developing more robust solutions.
-
Challenges in Using Multiple Inheritance:
- Mechanics: There's a lack of a systematic method to integrate the features of inherited classes. While multiple inheritance combines base classes, it often does so in a simplistic manner that doesn't suit complex scenarios. Designers usually need to carefully coordinate the interaction between inherited classes to achieve the desired functionality.
- Type Information: Base classes in a multiple inheritance setup often lack sufficient type information to perform their intended functions. For example, a
DeepCopybase class, designed to implement deep copying in a smart pointer, wouldn't inherently know the type of objects it needs to copy. - State Manipulation: When different behavioral aspects are implemented in base classes, they often need to manipulate a common state. This scenario typically requires virtual inheritance to share a state-holding base class, which can lead to more complex and rigid designs. This rigidity is contrary to the initial goal, which is to enable user classes to inherit from library classes, not the other way around.
-
Inadequacy of Multiple Inheritance for Design Flexibility:
- Despite its combinatorial nature, multiple inheritance alone is insufficient to address the vast array of design choices in software development.
1.4 The Benefit of Templates
The concept of policies and policy classes is crucial in creating safe, efficient, and highly customizable design elements in software engineering. Here's a summary with code examples illustrating this concept:
-
Policy Definition and Characteristics:
- A policy defines a class interface or a class template interface, including inner type definitions, member functions, and variables.
- Policies focus more on behavior than type, akin to traits, and are reminiscent of the Strategy design pattern but are bound at compile time.
-
Example of a Policy: The 'Creator' Policy:
- The
Creatorpolicy involves a class template for typeTwith aCreatefunction that returns a pointer toT. - Different implementations of this policy can create objects in various ways, such as using the
newoperator,mallocwith placementnew, or cloning a prototype object. - Example implementations:
template <class T> struct OpNewCreator { static T* Create() { return new T; } }; template <class T> struct MallocCreator { static T* Create() { void* buf = std::malloc(sizeof(T)); if (!buf) return 0; return new(buf) T; } }; template <class T> struct PrototypeCreator { PrototypeCreator(T* pObj = 0) : pPrototype_(pObj) {} T* Create() { return pPrototype_ ? pPrototype_->Clone() : 0; } private: T* pPrototype_; };
- The
-
Policy Classes and Implementations:
- Policy classes, the implementations of a policy, can be numerous and are meant to be used within other classes.
- Policies are loosely defined compared to classic interfaces and are syntax-oriented rather than signature-oriented.
-
Designing a Class Using the 'Creator' Policy:
- A class can be designed to utilize a policy by either containing or inheriting one of the policy class implementations.
- Example host class using a policy:
template <class CreationPolicy> class WidgetManager : public CreationPolicy { ... }; // Usage using MyWidgetMgr = WidgetManager<OpNewCreator<Widget>>;
-
Host Classes and Their Role:
- Host classes use one or more policies to assemble complex structures and behaviors.
- The
WidgetManagerclass is an example of a host class that allows users to configure specific aspects of its functionality through the chosen creation policy.
In essence, policy-based class design is about providing a framework where different behaviors can be plugged in as policies. This approach enables highly flexible and customizable designs, allowing users to tailor classes to specific needs without altering the fundamental structure of the class.
-
Templates for Combinatorial Behaviors:
- Templates are effective for code generation at compile time based on user-defined types or constant values. This makes them suitable for managing combinatorial behaviors in software design.
-
Customization with Class Templates:
- Class templates allow for extensive customization not possible with regular classes. For example, you can specialize member functions for specific instantiations of a class template.
- Partial template specialization is another feature, as shown in the code snippet:
template <class T, class U> class SmartPtr { ... }; // Specialization for Widget template <class U> class SmartPtr<Widget, U> { ... };
-
Limitations of Templates:
- Templates cannot be used to specialize the structure of a class, only functions.
- Partial specialization does not scale well with templates having multiple parameters. For instance:
template <class T> class Widget { void Fun() { /* generic implementation */ } }; template <> void Widget<char>::Fun() { /* specialized implementation */ } template <class T, class U> class Gadget { void Fun() { /* generic implementation */ } }; // This would result in an error template <class U> void Gadget<char, U>::Fun() { /* specialized implementation */ } - Only a single default implementation can be provided for each template member function.
-
Comparing Multiple Inheritance and Templates:
- Multiple inheritance and templates have complementary strengths and weaknesses.
- For example, multiple inheritance has limited mechanics and loses type information, while templates provide rich mechanics and retain type information.
- Specialization in templates doesn’t scale as well as multiple inheritance, and templates are limited to providing one default per member function, unlike multiple inheritance.
-
Combining Templates and Multiple Inheritance:
- The complementary nature of multiple inheritance and templates suggests that their combination could create a flexible and effective approach for designing libraries of design elements.
1.5 Policies and Policy Classes
- Policies and policy classes help in implementing safe, efficient, and highly customizable design elements.
A policy defines a class interface or a class template interface.
- The interface consists of one or all of the following: inner type definitions, member functions, and member variables.
- Policies have much in common with traits (Alexandrescu 2000a) but differ in that they put less emphasis on type and more emphasis on behavior.
- Also, policies are reminiscent of the Strategy design pattern (Gamma et al. 1995), with the twist that policies are bound at compilation time.
For example, let’s define a policy for creating objects.
- The
Creatorpolicy prescribes a class template of typeT. This class template must expose a member function calledCreatethat takes no arguments and returns a pointer toT. - Semantically, each call to
Createshould return a pointer to a new object of typeT. - The exact mode in which the object is created is left to the latitude of the policy implementation.
- Let’s define some policy classes that implement the
Creatorpolicy.- One possible way is to use the new operator.
- Another way is to use malloc and a call to the placement new operator (Meyers 1998b).
- Yet another way would be to create new objects by cloning a prototype object.
- Here are examples of all three methods:
// All structs below are policy classes/implementations
// that must have a T* Create() function
// such interface requirement is basically the "policy"
template <class T>
struct OpNewCreator {
static T* Create() { return new T; }
};
template <class T>
struct MallocCreator {
static T* Create() {
void* buf = std::malloc(sizeof(T));
if (!buf) return 0;
return new(buf) T;
}
};
template <class T>
struct PrototypeCreator {
PrototypeCreator(T* pObj = 0)
: pPrototype_(pObj) {}
T* Create() {
return pPrototype_ ? pPrototype_->Clone() : 0;
}
T* GetPrototype() {
return pPrototype_;
}
void SetPrototype(T* pObj) { pPrototype_ = pObj; }
private:
T* pPrototype_;
};
- For a given policy, there can be an unlimited number of implementations. The implementations of a policy are called policy classes.
- Policy classes are not intended for stand-alone use; instead, they are inherited by, or contained within, other classes.
- An important aspect is that, unlike classic interfaces (collections of pure virtual functions), policies’ interfaces are loosely defined.
Policies are syntax oriented, not signature oriented.
- In other words,
Creatorspecifies which syntactic constructs should be valid for a conforming class, rather than which exact functions that class must implement. - For example, the
Creatorpolicy does not specify thatCreatemust bestaticorvirtual—the only requirement is that the class template define aCreatemember function. - Also,
Creatorsays thatCreateshould return a pointer to a new object (as opposed to must). - Consequently, it is acceptable that in special cases,
Createmight return zero or throw an exception.
You can implement several policy classes for a given policy.
- They all must respect the interface as defined by the policy.
- The user then chooses which policy class to use in larger structures, as Part II of this book shows.
- The three policy classes defined earlier have different implementations and even slightly different interfaces (for example,
PrototypeCreatorhas two extra functions:GetPrototypeandSetPrototype). However, they all define a function calledCreatewith the required return type, so they conform to the Creator policy.
Let’s see now how we can design a class that exploits the
Creatorpolicy.
- Such a class will either contain or inherit one of the three classes defined previously, as shown in the following:
// Library code
template <class CreationPolicy>
class WidgetManager : public CreationPolicy {
...
};
The classes that use one or more policies are called hosts or host classes.
- In the example above,
WidgetManageris a host class with one policy. - Hosts are responsible for assembling the structures and behaviors of their policies in a single complex unit.
- When instantiating the
WidgetManagertemplate, the client passes the desired policy:
// Application code
using MyWidgetMgr = WidgetManager<OpNewCreator<Widget>>;
- Let’s analyze the resulting context. Whenever an object of type
MyWidgetMgrneeds to create aWidget, it invokesCreate()for itsOpNewCreator<Widget>policy subobject. - However, it is the user of
WidgetManagerwho chooses the creation policy. - Effectively, through its design,
WidgetManagerallows its users to configure a specific aspect ofWidgetManager’s functionality. This is the gist of policy-based class design.
1.5.1 Implementing Policy Classes with Template Template Parameters
- Often, as is the case above, the policy’s template argument is redundant.
- It is awkward that the user must pass
OpNewCreator’s template argument explicitly. - Typically, the host class already knows, or can easily deduce, the template argument of the policy class. In the example above,
WidgetManageralways manages objects of typeWidget, so requiring the user to specifyWidgetagain in the instantiation ofOpNewCreatoris redundant and potentially dangerous. - In this case, library code can use template template parameters for specifying policies, as shown in the following:
// Library code
template <template <class Created>
class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> { ...
};
- In spite of appearances, the
Createdsymbol does not contribute to the definition of WidgetManager. You cannot useCreatedinside WidgetManager—it is a formal argument forCreationPolicy(notWidgetManager) and can be simply omitted. - Application code now only needs to provide the name of the template in instantiating
WidgetManager:
// Application code
using MyWidgetMgr = WidgetManager<OpNewCreator>;
- Using template template parameters with policy classes is not simply a matter of convenience; sometimes, it is essential that the host class have access to the template so that the host can instantiate it with a different type.
- For example, assume
WidgetManageralso needs to create objects of typeGadgetusing the same creation policy. Then the code would look like this:
// Library code
template <template <class>
class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> { ...
void DoSomething() {
Gadget* pW = CreationPolicy<Gadget>().Create(); ...
}
};
- Does using policies give you an edge? At first sight, not a lot. For one thing, all implementations of the
Creatorpolicy are trivially simple. - The author of
WidgetManagercould certainly have written the creation code inline and avoided the trouble of makingWidgetManagera template.
But using policies gives great flexibility to
WidgetManager.
- First, you can change policies from the outside as easily as changing a template argument when you instantiate
WidgetManager. - Second, you can provide your own policies that are specific to your concrete application.
- You can use new, malloc, prototypes, or a peculiar memory allocation library that only your system uses.
- It is as if
WidgetManagerwere a little code generation engine, and you configure the ways in which it generates code. - To ease the lives of application developers,
WidgetManager’s author might define a battery of often-used policies and, in addition, provide a default template argument for the policy that’s most commonly used:
template <template <class>
class CreationPolicy = OpNewCreator>
class WidgetManager ...
Note that policies are quite different from mere virtual functions.
- Virtual functions promise a similar effect: The implementer of a class defines higher-level functions in terms of primitive
virtualfunctions and lets the user override the behavior of those primitives. - As shown above, however, policies come with enriched type knowledge and static binding, which are essential ingredients for building designs.
- Aren’t designs full of rules that dictate before runtime how types interact with each other and what you can and what you cannot do? Policies allow you to generate designs by combining simple choices in a typesafe manner.
- In addition, because the binding between a host class and its policies is done at compile time, the code is tight and efficient, comparable to its handcrafted equivalent.
- Of course, policies’ features also make them unsuitable for dynamic binding and binary interfaces, so in essence policies and classic interfaces do not compete.
1.5.2 Implementing Policy Classes with Template Member Functions
- An alternative to using template template parameters is to use template member functions in conjunction with simple classes.
- That is, the policy implementation is a simple class (as opposed to a template class) but has one or more templated members.
- For example, we can redefine the
Creatorpolicy to prescribe a regular (nontemplate) class that exposes a template functionCreate<T>. - A conforming policy class looks like the following:
struct OpNewCreator {
template <class T>
static T* Create() { return new T; } }
;
- This way of defining and implementing a policy has the advantage of being better supported by older compilers.
- On the other hand, policies defined this way might be harder to talk about, define, implement, and use.
1.6 Enriched Policies
- The
Creatorpolicy prescribes only one member function,Create. However,PrototypeCreatordefines two more functions:GetPrototypeandSetPrototype. Let’s analyze the resulting context. - Because
WidgetManagerinherits its policy class and becauseGetPrototypeandSetPrototypeare public members ofPrototypeCreator, the two functions propagate throughWidgetManagerand are directly accessible to clients. - However,
WidgetManagerasks only for theCreatemember function; that’s allWidgetManagerneeds and uses for ensuring its own functionality. Users, however, can exploit the enriched interface. - A user who uses a prototype-based Creator policy class can write the following code:
using MyWidgetManager = WidgetManager<PrototypeCreator> ;
Widget* pPrototype = ...;
MyWidgetManager mgr;
mgr.SetPrototype(pPrototype);
//... use mgr ...
- If the user later decides to use a creation policy that does not support prototypes, the compiler pinpoints the spots where the prototype-specific interface was used. This is exactly what should be expected from a sound design.
- The resulting context is very favorable. Clients who need enriched policies can benefit from that rich functionality, without affecting the basic functionality of the host class.
- Don’t forget that users—and not the library—decide which policy class to use. Unlike regular multiple interfaces, policies give the user the ability to add functionality to a host class, in a typesafe manner.
1.7 Destructors of Policy Classes
- There is an additional important detail about creating policy classes. Most often, the host class uses public inheritance to derive from its policies. For this reason, the user can automatically convert a host class pointer to a policy pointer and later delete the host class object through this pointer.
- Unless the policy class defines a
virtualdestructor, applying delete to a pointer to the policy class has undefined behavior, as shown below.
using MyWidgetManager = WidgetManager<PrototypeCreator> ; ...
MyWidgetManager wm;
PrototypeCreator<Widget>* pCreator = &wm; // dubious, but legal
delete pCreator; // compiles fine, but has undefined behavior
- Defining a virtual destructor for a policy, however, works against its static nature and hurts performance. Many policies don’t have any data members, but rather are purely behavioral by nature.
- The first virtual function added incurs some size overhead for the objects of that class, so the virtual destructor should be avoided.
- A solution is to have the host class use
protectedorprivateinheritance when deriving from the policy class. However, this would disable enriched policies as well (Section 1.6). - The lightweight, effective solution that policies should use is to define a nonvirtual protected destructor:
template <class T>
struct OpNewCreator {
static T* Create() {
return new T;
}
protected:
~OpNewCreator() {}
};
- Because the destructor is protected, only derived classes can destroy policy objects, so it’s impossible for outsiders to apply delete to a pointer to a policy class.
- The destructor, however, is not virtual, so there is no size or speed overhead.
1.8 Optional Functionality Through Incomplete Instantiation
- It gets even better. C contributes to the power of policies by providing an interesting feature. If a member function of a class template is never used, it is not even instantiated the compiler does not look at it at all, except perhaps for syntax checking.
- This gives the host class a chance to specify and use optional features of a policy class. For example, let’s define a
SwitchPrototypemember function forWidgetManager.
// Library code
template <template <class>
class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget> {
...
void SwitchPrototype(Widget* pNewPrototype) {
CreationPolicy<Widget>& myPolicy = *this;
delete myPolicy.GetPrototype();
myPolicy.SetPrototype(pNewPrototype);
}
};
- The resulting context is very interesting:
- If the user instantiates
WidgetManagerwith aCreatorpolicy class that supports prototypes, she can useSwitchPrototype. - According to the C standard, the degree of syntax checking for unused template functions is up to the implementation. The compiler does not do any semantic checking—for example, symbols are not looked up.
- If the user instantiates
WidgetManagerwith aCreatorpolicy class that does not support prototypes and tries to useSwitchPrototype, a compile-time error occurs. - If the user instantiates
WidgetManagerwith aCreatorpolicy class that does not support prototypes and does not try to useSwitchPrototype, the program is valid.
- If the user instantiates
- This all means that
WidgetManagercan benefit from optional enriched interfaces but still work correctly with poorer interfaces—as long as you don’t try to use certain member functions ofWidgetManager. - The author of
WidgetManagercan define the Creator policy in the following manner:- Creator prescribes a class template of one type
Tthat exposes a member functionCreate.Createshould return a pointer to a new object of typeT. - Optionally, the implementation can define two additional member functions—
T* GetPrototype()andSetPrototype(T*)—having the semantics of getting and setting a prototype object used for creation. - In this case,
WidgetManagerexposes theSwitchPrototype( T* pNewPrototype)member function, which deletes the current prototype and sets it to the incoming argument.
- Creator prescribes a class template of one type
- In conjunction with policy classes, incomplete instantiation brings remarkable freedom to you as a library designer. You can implement lean host classes that are able to use additional features and degrade graciously, allowing for Spartan, minimal policies.
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
SmartPtrclass template that uses two policies, as shown:
template <class T,
template <class> class CheckingPolicy,
template <class> class ThreadingModel>
class SmartPtr;
SmartPtrhas three template parameters: the pointee type and two policies.- Inside
SmartPtr, you orchestrate the two policies into a sound implementation. SmartPtrbecomes a coherent shell that integrates several policies, rather than a rigid, canned implementation.- By designing
SmartPtrthis way, you allow the user to configureSmartPtrwith a simple
using WidgetPtr = SmartPtr<Widget, NoChecking, SingleThreaded>;
- Inside the same application, you can define and use several smart pointer classes:
using SafeWidgetPtr = SmartPtr<Widget, EnforceNotNull, SingleThreaded>;
The two policies can be defined as follows:
- Checking:
- The
CheckingPolicy<T>class template must expose aCheckmember function, callable with an lvalue of typeT*. SmartPtrcallsCheck, passing it the pointee object before dereferencing it.
- The
- ThreadingModel:
- The
ThreadingModel<T>class template must expose an inner type calledLock, whose constructor accepts aT&. - For the lifetime of a Lock object, operations on the
Tobject are serialized.
- The
- For example, here is the implementation of the
NoCheckingandEnforceNotNullpolicy 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 pointer 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(); }
};
SmartPtruses 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
CheckingPolicyandThreadingModelpolicy 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.
1.10 Customizing Structure with Policy Classes
- One of the limitations of templates, mentioned in Section 1.4, is that you cannot use templates to customize the structure of a class—only its behavior. Policy-based designs, however, do support structural customization in a natural manner.
- Suppose that you want to support nonpointer representations for
SmartPtr. For example, on certain platforms some pointers might be represented by a handle—an integral value that you pass to a system function to obtain the actual pointer.- To solve this you might “indirect” the pointer access through a policy, say, a
Structurepolicy. - The
Structurepolicy abstracts the pointer storage. - Consequently,
Structureshould expose types calledPointerType(the type of a pointer to the pointed-to object) andReferenceType(the type of a reference to the pointed-to object) and functions such asGetPointerandSetPointer. - The fact that the pointer type is not hardcoded to
T*has important advantages. For example, you can useSmartPtrwith nonstandard pointer types (such as near and far pointers on segmented architectures), or you can easily implement clever solutions such asbeforeandafterfunctions (Stroustrup 2000a). The possibilities are extremely interesting. - The default storage of a smart pointer is a plain-vanilla pointer adorned with the
Structurepolicy interface, as shown in the following code.
- To solve this you might “indirect” the pointer access through a policy, say, a
template <class T>
class DefaultSmartPtrStorage {
public:
typedef T* PointerType;
typedef T& ReferenceType;
protected:
PointerType GetPointer() { return ptr_; }
void SetPointer(PointerType ptr) { ptr_ = ptr; }
private:
PointerType ptr_;
};
- The actual storage used is completely hidden behind
Structure’s interface. - Now
SmartPtrcan use aStoragepolicy instead of aggregating aT*.
template <class T,
template <class> class CheckingPolicy,
template <class> class ThreadingModel,
template <class> class Storage = DefaultSmartPtrStorage>
class SmartPtr;
- Of course,
SmartPtrmust either derive fromStorage<T>or aggregate aStorage<T>object in order to embed the needed structure.
1.11 Compatible and Incompatible Policies
- Suppose you create two instantiations of
SmartPtr:FastWidgetPtr, a pointer without checking, andSafeWidgetPtr, a pointer with checking before dereference. - An interesting question is:
- Should you be able to assign
FastWidgetPtrobjects toSafeWidgetPtrobjects? - Should you be able to assign them the other way around?
- If you want to allow such conversions, how can you implement that?
- Should you be able to assign
- Starting from the reasoning that
SafeWidgetPtris more restrictive thanFastWidgetPtr, it is natural to accept the conversion fromFastWidgetPtrtoSafeWidgetPtr. - This is because C already supports implicit conversions that increase restrictionsnamely, from non-const to const types.
- On the other hand, freely converting
SafeWidgetPtrobjects toFastWidgetPtrobjects is dangerous. This is because in an application, the majority of code would useSafeWidgetPtrand only a small, speed-critical core would useFastWidgetPtr. Allowing only explicit, controlled conversions to FastWidgetPtr would help keep FastWidgetPtr’s usage to a minimum. - The best, most scalable way to implement conversions between policies is to initialize and copy SmartPtr objects policy by policy, as shown below. (Let’s simplify the code by getting back to only one policy—the Checking policy.)
template <class T,
template <class> class CheckingPolicy>
class SmartPtr : public CheckingPolicy<T> {
template<class T1, template <class> class CP1>
SmartPtr(const SmartPtr<T1, CP1>& other)
: pointee_(other.pointee_)
, CheckingPolicy<T>(other) {
...
}
template <class T1, template<class> class CP1>
friend class SmartPtr;
};
SmartPtrimplements a templated copy constructor, which accepts any other instantiation ofSmartPtr. The code in bold initializes the components ofSmartPtrwith the components of the otherSmartPtr<T1,CP1>received as arguments.- Here’s how it works. (Follow the constructor code.) Assume you have a class
ExtendedWidget, derived fromWidget. If you initialize aSmartPtr<Widget,NoChecking>with aSmartPtr<ExtendedWidget, NoChecking>, the compiler attempts to initialize aWidget*with anExtendedWidget*(which works), and aNoCheckingwith aSmartPtr<ExtendedWidget, NoChecking>. - This might look suspicious, but don’t forget that
SmartPtrderives from its policy, so in essence the compiler will easily figure out that you initialize aNoCheckingwith aNoChecking. The whole initialization works. - Now for the interesting part. Say you initialize a
SmartPtr<Widget, EnforceNotNull>with aSmartPtr<ExtendedWidget, NoChecking>. TheExtendedWidget*toWidget*conversion works just as before. - Then the compiler tries to match
SmartPtr<ExtendedWidget, NoChecking>toEnforceNotNull’s constructors. IfEnforceNotNullimplements a constructor that accepts aNoCheckingobject, then the compiler matches that constructor. - If
NoCheckingimplements a conversion operator toEnforceNotNull, that conversion is invoked. In any other case, the code fails to compile. - As you can see, you have two-sided flexibility in implementing conversions between policies. You can implement a conversion constructor on the left-hand side, or you can implement a conversion operator on the right-hand side.
- The assignment operator looks like an equally tricky problem, but fortunately, Sutter (2000) describes a very nifty technique that allows you to implement the assignment operator in terms of the copy constructor. (It’s so nifty, you have to read about it. You can see the technique at work in Loki’s SmartPtr implementation.)
- Although conversions from
NoCheckingtoEnforceNotNulland even vice versa are quite sensible, some conversions don’t make any sense at all. Imagine converting a reference-counted pointer to a pointer that supports another ownership strategy, such as destructive copy (à lastd::auto_ptr).- Such a conversion is semantically wrong. The definition of reference counting is that all pointers to the same object are known and tracked by a unique counter.
- As soon as you try to confine a pointer to another ownership policy, you break the invariant that makes reference counting work.
- In conclusion, conversions that change the ownership policy should not be allowed implicitly and should be treated with maximum care. At best, you can change the ownership policy of a reference-counted pointer by explicitly calling a function. That function succeeds if and only if the reference count of the source pointer is.
1.12 Decomposing a Class into Policies
- The hardest part of creating policy-based class design is to correctly decompose the functionality of a class in policies.
- The rule of thumb is to identify and name the design decisions that take part in a class’s behavior. Anything that can be done in more than one way should be identified and migrated from the class to a policy.
- Don’t forget: Design constraints buried in a class’s design are as bad as magic constants buried in code.
- For example, consider a
WidgetManagerclass.- If
WidgetManagercreates new Widget objects internally, creation should be deferred to a policy. - If
WidgetManagerstores a collection of Widgets, it makes sense to make that collection a storage policy, unless there is a strong preference for a specific storage mechanism.
- If
At an extreme, a host class is totally depleted of any intrinsic policy.
- It delegates all design decisions and constraints to policies. Such a host class is a shell over a collection of policies and deals only with assembling the policies into a coherent behavior.
- The disadvantage of an overly generic host class is the abundance of template parameters.
- In practice, it is awkward to work with more than four to six template parameters.
- Still, they justify their presence if the host class offers complex, useful functionality.
Type definitions—
typedefstatements—are an essential tool in using classes that rely on policies.
- Using typedef is not merely a matter of convenience; it ensures ordered use and easy maintenance. For example, consider the following type definition:
typedef SmartPtr <Widget, RefCounted, NoChecked > WidgetPtr;
- It would be very tedious to use the lengthy specialization of
SmartPtrinstead ofWidgetPtrin code. But the tediousness of writing code is nothing compared with the major problems in understanding and maintaining that code. - As the design evolves,
WidgetPtr’s definition might change—for example, to use a checking policy other thanNoCheckedin debug builds. It is essential that all the code useWidgetPtrinstead of a hardcoded instantiation ofSmartPtr. - It’s just like the difference between calling a function and writing the equivalent
inlinecode: Theinlinecode technically does the same thing but fails to build an abstraction behind it.
When you decompose a class in policies, it is very important to find an orthogonal decomposition.
- An orthogonal decomposition yields policies that are completely independent of each other. You can easily spot a nonorthogonal decomposition when various policies need to know about each other.
- For example, think of an
Arraypolicy in a smart pointer. TheArraypolicy is very simple—it dictates whether or not the smart pointer points to an array.- The policy can be defined to have a member function
T& ElementAt(T* ptr, unsigned int index), plus a similar version forconst T. - The non-array policy simply does not define an
ElementAtmember function, so trying to use it would yield a compile-time error. - The
ElementAtfunction is an optional enriched behavior as defined in Section 1.6.
- The policy can be defined to have a member function
- The implementations of two policy classes that implement the Array policy follow.
template <class T> struct IsArray {
T& ElementAt(T* ptr, unsigned int index) { return ptr[index]; }
const T& ElementAt(T* ptr, unsigned int index) const { return ptr[index]; }
};
template <class T>
struct IsNotArray {};
- The problem is that purpose of the
Arraypolicy—specifying whether or not the smart pointer points to an array—interacts unfavorably with another policy: destruction. - You must destroy pointers to objects using the
deleteoperator, and destroy pointers to arrays of objects using thedelete[]operator. - Two policies that do not interact with each other are orthogonal. By this definition, the Array and the Destroy policies are not orthogonal.
- If you still need to confine the qualities of being an array and of destruction to separate policies, you need to establish a way for the two policies to communicate. You must have the
Arraypolicy expose a Boolean constant in addition to a function, and pass that Boolean to the Destroy policy. This complicates and somewhat constrains the design of both the Array and the Destroy policies. - Nonorthogonal policies are an imperfection you should strive to avoid. They reduce compile-time type safety and complicate the design of both the host class and the policy classes.
- If you must use nonorthogonal policies, you can minimize dependencies by passing a policy class as an argument to another policy class’s template function. This way you can benefit from the flexibility specific to template-based interfaces.
- The downside remains that one policy must expose some of its implementation details to other policies. This decreases encapsulation.
1.13 Summary
- Design is choice.
- Most often, the struggle is not that there is no way to solve a design problem, but that there are too many ways that apparently solve the problem.
- You must know which collection of solutions solves the problem in a satisfactory manner. The need to choose propagates from the largest architectural levels down to the smallest unit of code. Furthermore, choices can be combined, which confers on design an evil multiplicity.
- To fight the multiplicity of design with a reasonably small amount of code, a writer of a design-oriented library needs to develop and use special techniques.
- These techniques are purposely conceived to support flexible code generation by combining a small number of primitive devices.
- The library itself provides a number of such devices.
- Furthermore, the library exposes the specifications from which these devices are built, so the client can build her own.
- This essentially makes a policy-based design open-ended.
- These devices are called policies, and the implementations thereof are called policy classes.
- The mechanics of policies consist of a combination of templates with multiple inheritance.
- A class that uses policies—a host class
- is a template with many template parameters (often, template template parameters), each parameter being a policy.
- The host class “indirects” parts of its functionality through its policies and acts as a receptacle that combines several policies in a coherent aggregate.
- Classes designed around policies support enriched behavior and graceful degradation of functionality.
- A policy can provide supplemental functionality that propagates through the host class due to public inheritance.
- Furthermore, the host class can implement enriched functionality that uses the optional functionality of a policy.
- If the optional functionality is not present, the host class still compiles successfully, provided the enriched functionality is not used.
- The power of policies comes from their ability to mix and match. A policy-based class can accommodate very many behaviors by combining the simpler behaviors that its policies implement. This effectively makes policies a good weapon for fighting against the evil multiplicity of design.
- Using policy classes, you can customize not only behavior but also structure.
- This important feature takes policy-based design beyond the simple type genericity that’s specific to container classes.
- Policy-based classes support flexibility when it comes to conversions.
- If you use policy-by-policy copying, each policy can control which other policies it accepts, or converts to, by providing the appropriate conversion constructors, conversion operators, or both.
- In breaking a class into policies, you should follow two important guidelines.
- One is to localize, name, and isolate design decisions in your class—things that are subject to a trade-off or could be sensibly implemented in various ways.
- The other guideline is to look for orthogonal policies, that is, policies that don’t need to interact with each other and that can be changed independently.