Almost policy-based approach

We are now going to look at an alternative to the policy-based design we have been studying so far. It is not as general, but when it works, it can provide all the advantages of the policies, in particular, the composability, without some of the problems. To introduce this new approach, we will consider the problem of designing a custom value type. 

A value type, to put it simply, is a type that behaves mostly like an int. Often, these types are numbers. While we have a set of built-in types for that, we may want to operate on rational numbers, complex numbers, tensors, matrices, or numbers that have units associated with them (meters, grams, and so on). These value types support a set of operations such as arithmetic operations, comparisons, assignment, and copying. Depending on what the value represents, we may need only a limited subset of these operations—for example, we may need to support addition and multiplication for matrices, but no division, and comparing matrices for anything other than equality probably doesn't make sense in most cases. Similarly, we probably don't want to allow the addition of meters to grams.

More generally, there is often a desire to have a numeric type with a limited interface—we would like it if the operations that we do not wish to allow for the quantity represented by such numbers did not compile. This way, a program with an invalid operation simply cannot be written. 

This problem can be tackled with a policy-based approach:

template <typename T, 
typename AdditionPolicy, typename ComparisonPolicy,
typename OrderPolicy, typename AssignmentPolicy, ..... >
class Value { ..... };

This implementation runs into the entire set of drawbacks of policy-based design—the policy list is long, all policies must be spelled out, and there aren't any good defaults; the policies are positional, so the type declaration requires careful counting of commas, and, as the new policies are added, any semblance of a meaningful order of policies disappears. Note that we did not mention the problem of different sets of policies creating different types—in this case, this is not a drawback, but the design intent. If we want a type with support for addition and a similar type but without addition, these have to be different types. 

Ideally, we would like to just list the properties we want our value to have—I want a value type based on integers that support addition and multiplication and assignment, but nothing else. As it turns out, there is a way to accomplish this. 

First, let's think of what such a policy might look like. For example, the policy that enables addition should inject operator+() into the public interface of the class (and maybe also operator+=()). The policy that makes the value assignable should inject operator=(). We have seen enough of such policies to know how they are implemented—they have to be base classes, publicly inherited, and they need to know what the derived class is and cast it to its type, so they have to use CRTP:

template <typename T,     // T is the foundation type (like int)
typename V> // V is the derived class
struct Incrementable
{
V operator++() {
V& v = static_cast<V&>(*this);
++v.value_; // This is the actual value
// inside the derived class
return v;
}
};

Now, we need to give some thought to the use of these policies in the primary template. First of all, we want to support the unknown number of policies, in any order. This brings variadic templates to mind. However, to use CRTP, the template parameters have to be templates themselves. Then, we want to inherit from an instantiation of each of these templates, however many there are. What we need is a variadic template with a template template parameter pack:

template <typename T, template <typename, typename> class ... Policies>
class Value : public Policies<T, Value<T, Policies ... >> ...
{ ..... }; // Not three dots!

We have to be careful with ellipses (...) from now on—in the previous sections, we used ..... to indicate some more code here that we have seen already and don't want to repeat. But from now on, three dots (...) is literally three dots—it's part of the C++ syntax for variadic templates (this is why the rest of this chapter used five dots to indicate more code and not three). The preceding declaration introduces a class template called Value, with at least one parameter that is a type, plus zero or more template policies, which themselves have two type parameters (again, in C++17, we can also write typename ... Policies instead of class ... Policies). The Value class instantiates these templates with the type T and itself, and inherits publicly from all of them.  

The Value class template should contain the interface that we want to be common for all our value types. The rest will have to come from policies. Let's make the values copyable, assignable, and printable by default:

template <typename T, template <typename, typename> class ... Policies>
class Value : public Policies<T, Value<T, Policies ... >> ...
{
public:
typedef T value_type;
explicit Value() : val_(T()) {}
explicit Value(T v) : val_(v) {}
Value(const Value& rhs) : val_(rhs.val_) {}
Value& operator=(Value rhs) { val_ = rhs.val_; return *this; }
Value& operator=(T rhs) { val_ = rhs; return *this; }
friend std::ostream& operator<<(std::ostream& out, Value x) {
out << x.val_; return out;
}
friend std::istream& operator>>(std::istream& in, Value& x) {
in >> x.val_; return in;
}

private:
T val_;
};

The stream inserter operators << and >> have to be non-member functions, as usual. We use the friend factory, which was described in the Chapter 12, Friend Factory to generate these functions. 

Before we can indulge ourselves in implementing all of the policies, there is one more hurdle to overcome. The val_ value is private in the Value class, and we like it this way. However, the policies need to access it and modify it. In the past, we solved this problem by making each policy that needed such access into a friend. This time, we don't even know the names of the policies we may have. After working through the preceding declaration of the parameter pack expansion as a set of base classes, the reader may reasonably expect us to pull a rabbit out of the hat and somehow declare friendship to the entire parameter pack. Unfortunately, we know of no such way. The best solution we can suggest is to provide a set of accessor functions that should be called only by the policies, but there is no good way to enforce that (a name, such as policy_accessor_do_not_call(), might go some way to suggest that the user code should stay away from it, but the ingenuity of the programmer knows no bounds, and such hints are not universally respected):

template <typename T, template <typename, typename> class ... Policies>
class Value : public Policies<T, Value<T, Policies ... >> ...
{
public:
.....
T get() const { return val_; }
T& get() { return val_; }
private:
T val_;
};

To create a value type with a restricted set of operations, we have to instantiate this template with a list of policies we want, and nothing else:

using V = Value<int, Addable, Incrementable>;
V v1(0), v2(1);
v1++; // Incrementable - OK
V v3(v1 + v2); // Addable - OK
v3 *= 2; // No multiplication policies - won't compile

The number and the type of policies we can implement is limited mostly by the need at hand (or imagination), but here are some examples that demonstrate adding different kinds of operations to the class.

First of all, we can implement the aforementioned Incrementable policy that provides the two ++ operators, postfix and prefix:

template <typename T, typename V>
struct Incrementable
{
V operator++() {
V& v = static_cast<V&>(*this);
++(v.get());
return v;
}
V operator++(int) {
V& v = static_cast<V&>(*this);
return V(v.get()++);
}
};

We can make a separate Decrementable policy for the -- operators, or have one policy for both if it makes sense for our type. Also, if want to increment by some value other than one, then we need the += operators as well: 

template <typename T, typename V>
struct Incrementable
{
V& operator+=(V val) {
V& v = static_cast<V&>(*this);
v.get() += val.get();
return v;
}
V& operator+=(T val) {
V& v = static_cast<V&>(*this);
v.get() += val;
return v;
}
};

The preceding policy provides two versions of the operator+=()—one accepts the increment of the same Value type, and the other of the foundation type T. This is not a requirement, and we could implement an increment by values of some other types as needed. We can even have several versions of the increment policy, as long as only one is used (the compiler would let us know if we were introducing incompatible overloads of the same operator).

We can add the operators *= and /= in a similar manner. Adding binary operators such as comparison operators or addition and multiplication is a little different—these operators have to be non-member functions to allow for type conversions on the first argument. Again, the friend factory pattern comes in handy. Let's start with the comparison operators:

template <typename T, typename V> 
struct ComparableSelf
{
friend bool operator==(V lhs, V rhs) { return lhs.get() == rhs.get(); }
friend bool operator!=(V lhs, V rhs) { return lhs.get() != rhs.get(); }
};

When instantiated, this template generates two non-member non-template functions, that is, the comparison operators for variables of the type of the specific Value class, the one that is instantiated. We may also want to allow comparisons with the foundation type (such as int):

template <typename T, typename V> 
struct ComparableValue
{
friend bool operator==(V lhs, T rhs) { return lhs.get() == rhs; }
friend bool operator==(T lhs, V rhs) { return lhs == rhs.get(); }
friend bool operator!=(V lhs, T rhs) { return lhs.get() != rhs; }
friend bool operator!=(T lhs, V rhs) { return lhs != rhs.get(); }
};

More often than not, we will likely want both types of comparison at the same time. We could simply put them both into the same policy and not worry about separating them, or we could create a combined policy from the two we already have:

template <typename T, typename V>
struct Comparable : public ComparableSelf<T, V>,
public ComparableValue<T, V>
{
};

The addition and multiplication operators are created by similar policies. They are also friendly non-template non-member functions. The only difference is the return value type—they return the object itself, for example:

template <typename T, typename V>
struct Addable
{
friend V operator+(V lhs, V rhs) { return V(lhs.get() + rhs.get()); }
friend V operator+(V lhs, T rhs) { return V(lhs.get() + rhs); }
friend V operator+(T lhs, V rhs) { return V(lhs + rhs.get()); }
};

Explicit or implicit conversion operators can be added as well; the policy is very similar to the one we already used for pointers:

template <typename T, typename V>
struct ExplicitConvertible
{
explicit operator T() {
return static_cast<V*>(this)->get();
}
explicit operator const T() const {
return static_cast<const V*>(this)->get();
}
};

This approach, at first glance, seems to solve most of the drawbacks of the policy-based types (except for one of them being separate types, of course). The order of the policies does not matter—we can specify only the ones we want and not worry about the other ones—what's not to like? There are, however, two fundamental limitations. First of all, the policy-based class cannot refer to any policy by name. There is no longer a slot for DeletionPolicy or AdditionPolicy. There are no convention-enforced policy interfaces, such as the deletion policy having to be callable. The entire process of binding the policies into the single type is implicit; it's just a superposition of interfaces.

Therefore, we are limited in what we can do using these policies—we can inject public member functions and non-member functions—even add private data members—but we cannot provide an implementation for an aspect of behavior that's determined and limited by the primary policy-based class. As such, this is not an implementation of the Strategy pattern—we are composing the interface (and, necessarily, the implementation) at will, not customizing a specific algorithm.

The second, closely related, limitation is that there are no default policies. The missing policies are just that, missing. There is nothing in their place. The default behavior is always the absence of any behavior. In the traditional policy-based design, each policy slot has to be filled. If there is a reasonable default, it can be specified. Then, that is the policy, unless the user overrides it (for example, the default deletion policy uses operator delete). If there is no default, the compiler won't let us omit the policy—we have to give an argument to the template. 

The consequences of these limitations reach farther than the reader may think at first glance. For example, it may be tempting to use the enable_if technique instead of injecting public member functions through the base class. Then, we could have a default behavior that is enabled if none of the other options are. But it won't work here. We can certainly create a policy that is targeted for use with enable_if

template <typename T, typename V> struct Addable
{
constexpr bool adding_enabled = true;
};

But there is no way to use it—we can't use AdditionPolicy::adding_enabled because there is no AdditionPolicy—all policy slots are unnamed. The other option would be to use Value::adding_enabled—the addition policy is a base class of Value, and, therefore, all of its data members are visible in the Value class. The only problem is that it does not work—at the point where this expression is evaluated by the compiler (in the definition of the Value type as the template parameter for the CRTP policies), Value is an incomplete type and we cannot access its data members yet. We could evaluate policy_name::adding_enabled if we knew what the policy name was. But that knowledge is exactly what we gave up in trade for not having to specify the entire list of policies. 

While not, strictly speaking, an application of the Strategy pattern, the alternative to the policy-based design that we have just learned about can be attractive when the policies are primarily used to control a set of supported operations. While discussing the guidelines for policy-based design, we have mentioned that it is rarely worth it to use a policy slot just to provide the additional safety of the restricted interface. For such situations, this alternative approach should be kept in mind. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.44.52