Cpp Notes

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 SingletonHolder class template utilizes policies for lifetime management and thread safety.

  • Chapter 7's SmartPtr is 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Library Rigidity and Intellectual Overhead:

    • Highly specific library solutions fail to accommodate slight customization needs, rendering them ineffective for slightly different use cases.
  7. 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.
  8. 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.
  9. 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?

  1. Multiple Inheritance for Combining Features:

    • The concept of a TemporarySecretary class, inheriting from both Secretary and Temporary classes, 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, and RefCounted.
  2. 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.
  3. 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 DeepCopy base 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.
  4. 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:

  1. 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.
  2. Example of a Policy: The 'Creator' Policy:

    • The Creator policy involves a class template for type T with a Create function that returns a pointer to T.
    • Different implementations of this policy can create objects in various ways, such as using the new operator, malloc with placement new, 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_; 
      };
  3. 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.
  4. 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>>;
  5. Host Classes and Their Role:

    • Host classes use one or more policies to assemble complex structures and behaviors.
    • The WidgetManager class 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.

  1. 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.
  2. 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> { ... };
  3. 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.
  4. 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.
  5. 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 Creator policy prescribes a class template of type T. This class template must expose a member function called Create that takes no arguments and returns a pointer to T.
  • Semantically, each call to Create should return a pointer to a new object of type T.
  • 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 Creator policy.
    • 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, Creator specifies which syntactic constructs should be valid for a conforming class, rather than which exact functions that class must implement.
  • For example, the Creator policy does not specify that Create must be static or virtual—the only requirement is that the class template define a Create member function.
  • Also, Creator says that Create should return a pointer to a new object (as opposed to must).
  • Consequently, it is acceptable that in special cases, Create might 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, PrototypeCreator has two extra functions: GetPrototype and SetPrototype). However, they all define a function called Create with the required return type, so they conform to the Creator policy.

Let’s see now how we can design a class that exploits the Creator policy.

  • 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, WidgetManager is 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 WidgetManager template, the client passes the desired policy:
// Application code 
using MyWidgetMgr = WidgetManager<OpNewCreator<Widget>>;
  • Let’s analyze the resulting context. Whenever an object of type MyWidgetMgr needs to create a Widget, it invokes Create() for its OpNewCreator<Widget> policy subobject.
  • However, it is the user of WidgetManager who chooses the creation policy.
  • Effectively, through its design, WidgetManager allows its users to configure a specific aspect of WidgetManager’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, WidgetManager always manages objects of type Widget, so requiring the user to specify Widget again in the instantiation of OpNewCreator is 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 Created symbol does not contribute to the definition of WidgetManager. You cannot use Created inside WidgetManager—it is a formal argument for CreationPolicy (not WidgetManager) 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 WidgetManager also needs to create objects of type Gadget using 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 Creator policy are trivially simple.
  • The author of WidgetManager could certainly have written the creation code inline and avoided the trouble of making WidgetManager a 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 WidgetManager were 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 virtual functions 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 Creator policy to prescribe a regular (nontemplate) class that exposes a template function Create<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 Creator policy prescribes only one member function, Create. However, PrototypeCreator defines two more functions: GetPrototype and SetPrototype. Let’s analyze the resulting context.
  • Because WidgetManager inherits its policy class and because GetPrototype and SetPrototype are public members of PrototypeCreator, the two functions propagate through WidgetManager and are directly accessible to clients.
  • However, WidgetManager asks only for the Create member function; that’s all WidgetManager needs 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 virtual destructor, 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 protected or private inheritance 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 SwitchPrototype member function for WidgetManager.
// 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 WidgetManager with a Creator policy class that supports prototypes, she can use SwitchPrototype.
  • 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 WidgetManager with a Creator policy class that does not support prototypes and tries to use SwitchPrototype, a compile-time error occurs.
    • If the user instantiates WidgetManager with a Creator policy class that does not support prototypes and does not try to use SwitchPrototype, the program is valid.
  • This all means that WidgetManager can benefit from optional enriched interfaces but still work correctly with poorer interfaces—as long as you don’t try to use certain member functions of WidgetManager.
  • The author of WidgetManager can define the Creator policy in the following manner:
    • Creator prescribes a class template of one type T that exposes a member function Create. Create should return a pointer to a new object of type T.
    • Optionally, the implementation can define two additional member functions—T* GetPrototype() and SetPrototype(T*)—having the semantics of getting and setting a prototype object used for creation.
    • In this case, WidgetManager exposes the SwitchPrototype( T* pNewPrototype) member function, which deletes the current prototype and sets it to the incoming argument.
  • 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 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 becomes 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
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 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 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(); } 
};
  • 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.

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 Structure policy.
    • The Structure policy abstracts the pointer storage.
    • Consequently, Structure should expose types called PointerType (the type of a pointer to the pointed-to object) and ReferenceType (the type of a reference to the pointed-to object) and functions such as GetPointer and SetPointer.
    • The fact that the pointer type is not hardcoded to T* has important advantages. For example, you can use SmartPtr with nonstandard pointer types (such as near and far pointers on segmented architectures), or you can easily implement clever solutions such as before and after functions (Stroustrup 2000a). The possibilities are extremely interesting.
    • The default storage of a smart pointer is a plain-vanilla pointer adorned with the Structure policy interface, as shown in the following code.
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 SmartPtr can use a Storage policy instead of aggregating a T*.
template <class T,
				  template <class> class CheckingPolicy,
					template <class> class ThreadingModel,
					template <class> class Storage = DefaultSmartPtrStorage> 
class SmartPtr;
  • Of course, SmartPtr must either derive from Storage<T> or aggregate a Storage<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, and SafeWidgetPtr, a pointer with checking before dereference.
  • An interesting question is:
    • Should you be able to assign FastWidgetPtr objects to SafeWidgetPtr objects?
    • Should you be able to assign them the other way around?
    • If you want to allow such conversions, how can you implement that?
  • Starting from the reasoning that SafeWidgetPtr is more restrictive than FastWidgetPtr, it is natural to accept the conversion from FastWidgetPtr to SafeWidgetPtr.
  • This is because C already supports implicit conversions that increase restrictionsnamely, from non-const to const types.
  • On the other hand, freely converting SafeWidgetPtr objects to FastWidgetPtr objects is dangerous. This is because in an application, the majority of code would use SafeWidgetPtr and only a small, speed-critical core would use FastWidgetPtr. 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;
};
  • SmartPtr implements a templated copy constructor, which accepts any other instantiation of SmartPtr. The code in bold initializes the components of SmartPtr with the components of the other SmartPtr<T1,CP1> received as arguments.
  • Here’s how it works. (Follow the constructor code.) Assume you have a class ExtendedWidget, derived from Widget. If you initialize a SmartPtr<Widget,NoChecking> with a SmartPtr<ExtendedWidget, NoChecking>, the compiler attempts to initialize a Widget* with an ExtendedWidget* (which works), and a NoChecking with a SmartPtr<ExtendedWidget, NoChecking>.
  • This might look suspicious, but don’t forget that SmartPtr derives from its policy, so in essence the compiler will easily figure out that you initialize a NoChecking with a NoChecking. The whole initialization works.
  • Now for the interesting part. Say you initialize a SmartPtr<Widget, EnforceNotNull> with a SmartPtr<ExtendedWidget, NoChecking>. The ExtendedWidget* to Widget* conversion works just as before.
  • Then the compiler tries to match SmartPtr<ExtendedWidget, NoChecking> to EnforceNotNull’s constructors. If EnforceNotNull implements a constructor that accepts a NoChecking object, then the compiler matches that constructor.
  • If NoChecking implements a conversion operator to EnforceNotNull, 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 NoChecking to EnforceNotNull and 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 (à la std::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 WidgetManager class.
    • If WidgetManager creates new Widget objects internally, creation should be deferred to a policy.
    • If WidgetManager stores 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.

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—typedef statements—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 SmartPtr instead of WidgetPtr in 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 than NoChecked in debug builds. It is essential that all the code use WidgetPtr instead of a hardcoded instantiation of SmartPtr.
  • It’s just like the difference between calling a function and writing the equivalent inline code: The inline code 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 Array policy in a smart pointer. The Array policy 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 for const T.
    • The non-array policy simply does not define an ElementAt member function, so trying to use it would yield a compile-time error.
    • The ElementAt function is an optional enriched behavior as defined in Section 1.6.
  • 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 Array policy—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 delete operator, and destroy pointers to arrays of objects using the delete[] 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 Array policy 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.