Matthieu Gilson offers some thoughts on using a policy based mechanism to build efficient classes from loosely coupled components.
The problem considered in this paper is not new and relates to generic programming: how to build a class made of several 'components' that can have various implementations where these components can communicate with each other, while keeping the design of the components independent? For instance, an object would consist of an update mechanism plus a data structure, where both belong to a family of various mechanisms and data structures (distinct mechanisms perform distinct processes, etc.; thus some couples of mechanism-data are compatible and others are not).
To be more precise, such components would be implemented in classes (or similar entities) and use methods from other 'classes', while their design is kept orthogonal. The introduction illustrates this problem and then presents a solution using member function templates.
An alternative solution (so-called Policy Bridge) is then presented, where the 'components' are implemented in policies [ Alexandrescu01 ], [ Vandevoorde/Josuttis03 ] and the pattern 'bridges' them to construct an instantiated type. The term 'bridge' here refers to the usual Bridge pattern: the instantiated type can be seen as an interface that inherits the functionality of all the policies it is made of. The compatibility between the policies (for instance checking the interface at a call of a function from another policy) is checked at compile time. A common interface can be implemented with the cost of a virtual function for each call.
So far, the functionality achieved by the policy bridge is similar to the ones using member function templates and a brief comparison with meta-functions is explored as well. The code may be heavier but in a sense the design is more modular with policies. A combination of policies with meta-functions, to bring more genericity to encode the components, is then introduced. Policy-based visitors that enable visiting of some of the policies involved in the construction of an object via its interface.
A generic version of the policy bridge is then presented and the automation of such a code for an arbitrary number of policies (using macros from the preprocessor library of Boost [ Boost ]).
The example code runs under g++ (3.4.4 and 4.02), Visual C++ (7.1 and 8), and Comeau.
Introduction
Let us play with neurons. Such objects can be abstracted mathematically, with various features and mechanisms to determine their activity: activation mechanism (their internal update method); synapses to connect them; etc. In order to simulate a network made of various types of neurons interconnected with various types of connections, and keep the design as flexible as possible to be able to combine the different mechanisms together at will.
Without even going as far as connecting neurons, a fair number of design issues arise. Say we have a data structure which represents a feature of one type of neuron:
class Data1
{
public:
Data1(double a) : a_(a) {}
const double & a() {return a_;}
protected:
const double a_;
};
and an update mechanism to model the state of our neuron where the update method uses the parameter a implemented in the data:
class UpdateMech1
{
public:
const double & state() const {return state_;}
UpdateMech1() : state_(0.) {}
void update(double a) {state_ += a;}
protected:
double state_;
};
Our neuron is somehow the 'merging' of both classes, and another update method at the level of Neuron1 has to be declared so as to link the parameter of UpdateMech1::update with Data1::a() :
class Neuron1
: public Data1
, public UpdateMech1
{
public:
Neuron1(double a) : Data1(a) {}
void update() {UpdateMech1::update(Data1::a());}
};
To make it more generic for various data and mechanisms (say we have Data1 and Data2 , UpdateMech1 and UpdateMech2 ), we can templatise the features and mechanisms:
template <class Data, class UpdateMech>
class Neuron
: public Data
, public UpdateMech
{
...
void update() {UpdateMech::update(Data::a());}
};
Yet, all of the DataX would now require a method a and all the UpdateMechX would require an update method with a double as parameter, which is not flexible. The alternative using abstract base classes for both the family of the data classes ( DataBase ) and the family of the update mechanisms ( UpdateMechBase ), while Neuron would hold one member variable for each family, would face similar limitations:
class Neuron
{
...
void update() {u_.update(d_.a());}
DataBase * d_;
UpdateMechBase * u_;
};
Indeed, the interface DataBase should have a pure virtual method a :
struct DataBase
{
virtual const double & a() = 0;
};
and this would cause the interface to be rigid (even more than with the template version): it could be necessary for other update mechanisms to get a non-constant a , which does not fit this method a ; if some more variables are needed by only a particular UpdateMechX , the whole common interface still has to be modified; etc. Actually, the family of the data classes needs no common interface here, provided that for each combination with a specific UpdateMechX , the latter knows how to call the variable it needs. On the contrary, the machinery implemented in DataX and UpdateMechX and their way to communicate with each other should remain as unconstrained as possible (in terms of constructors, etc.), only Neuron would need a common interface in the end.
Speed could be another reason to discard this alternative choice, since the template version does not require virtual functions, and thus the internal mechanisms remain fast (only function calls).
The former version of
Neuron
can be modified
to make the combinations between the
DataX
and
the
UpdateMechX
more flexible. An option is to
change
UpdateMechX
into class templates where
DataX
is the template argument of
UpdateMechX
,
and to derive them from the classes
DataX
, as
shown in Listing 1.
class Data1 template <class Data> template <class UpdateMech> Neuron<UpdateMech1<Data1> > nrn1; |
Listing 1 |
This way, we can define various update mechanisms and various data classes, and combine them together. The Data family and the UpdateMech family are kept somehow independent in terms of design: basically, UpdateMech1 only requires to know the name of the method a implemented in all of the DataX to be combined with UpdateMech1 ; type checks require that a returns a type convertible into const double .
The construction
Neuron<UpdateMech<Data>
>
still has quite strong limitations, in the sense that
Data
and
UpdateMech
are no
longer on the same level in the derivation tree above
Neuron
.
Imagine that one of the data classes also needs to use a method that
belongs to the family of the
UpdateMechX
. Or
imagine there are not just one update mechanism but two (e.g. a
learning mechanism
PlasticityMech
in addition to
the
UpdateMech
for some specific neurons), and
they both would have to use something from the
Data
class family; both would derive from
Data
, and
Neuron
would derive from both, so the derivation
from
Data
must be virtual, as shown in Listing 2.
class Data1; template <class Data> template <class Data> template <class UpdateMech, class PlasticityMech> |
Listing 2 |
Since there was no virtual machinery before, this may imply costs. But even if we would not care, there is a last reason to seek for another solution: there may eventually be a need to implement mechanisms with 'new' interactions that we cannot really predict at this stage; and any further change would possibly mess up our beautiful (but rigid) derivation tree.
Let us go back to thinking of our design and what we wanted of it. The key idea here is that UpdateMech1 requires that Neuron has a method a that is used by UpdateMech1 , but the fact that Neuron inherits a from Data1 does not matter to UpdateMech1 . Other mechanisms may have 'requirements', which makes mechanisms compatible or not (to be detected at compile time). They would need to be all at the 'same level' in the derivation tree, i.e. any mechanism or feature is equally likely to need something in another one. In other words, we want a design likely to combine any compatible update and data 'classes' in a flexible way and thus define a neuron out of them; these mechanisms can communicate together while they are kept as 'orthogonal' as possible (they only know the name of the methods belonging to other mechanisms they need to call); and as few virtual functions as possible should be involved in the internal machinery (within Neuron ), to try to keep fast computation.
One solution can be to use member function templates (in the
classes that define the features or mechanisms) for the methods that
require a feature from outside their scope (Listing 3) and to call
update_impl
from the class
Neuron
with
*this
as an argument (Listing 4).
class UpdateMech1 class Data1 |
Listing 3 |
template <class Data, class UpdateMech> |
Listing 4 |
Note that UpdateMech1 has to be a friend of Data1 if a_impl is protected. We would need to add similar declarations for any other mechanism to be combined with Data1 , or define all the methods as public in the features and mechanisms. Another slight drawback is that the calls to the member function templates must come from Neuron , which means that having a pointer to one of the mechanisms (such as UpdateMech ) is not sufficient to call the update method if it is a template method (we still would need to pass the Neuron object as an argument).
In the end, this solution is valid and would be suitable for our requirements. Yet, we will now consider an alternative design, which brings further possibilities. The trade-off is mainly the increase in complexity in the design.
Details of the design
The purpose is still to allow any mechanism to call 'directly' any method from another 'mechanism' from which Neuron derives (via the class Neuron ). To give a rough idea, instead of the function template nested in the mechanism class ( UpdateMech1::update_impl<class TypeImpl>(TypeImpl &) ), the whole class UpdateMech1 is now a class template with a non template function ( UpdateMech1<class TypeImpl>::update_impl() ). This idea has something to do with the well-known trick used together with curiously recurring pattern (CRTP [ Alexandrescu01 ], [ Vandevoorde/Josuttis03 ]):
template <class TypeImpl>
class UpdateMech
{
void update_impl()
{
state_ += static_cast<TypeImpl &>(*this).a();
}
};
Defined this way, UpdateMech can be seen as a policy [ Alexandrescu01 ], [ Vandevoorde/Josuttis03 ] on the implemented type TypeImpl with the template argument Neuron . It allows us to call the method a that Neuron inherits from Data by means of static_cast , which converts the self-reference *this to the instantiated policy object back to Neuron .
This use of policies is somewhat different from [
Alexandrescu01
]: a
usual example is the pointer class
SmartPtr
defined on a type
T
(to make it shorter than
TypeImpl
) and some properties of the pointers can be
implemented by policies. For instance, let us build a policy to count
the references of the pointed element (Listing 5) and the smart pointer
class template is then as shown in Listing 6.
template <class T> |
Listing 5 |
template <class T |
Listing 6 |
The policy CountRef here is a mechanism that uses the type T , and SmartPtr uses T and CountRef , so there is no recursion in the design. However, we want to build Neuron which uses DataPolicy<Neuron> and UpdatePolicy<Neuron> .
First, we rewrite the mechanisms
Data1
and
UpdateMech1
as policies (see Listing 7).
template <class TypeImpl> template <class TypeImpl> |
Listing 7 |
Note that the typdedef VarTypeInit was added in Data1 to define the type of the parameter required to initialise this policy.
Then the
Neuron
pattern straightforwardly
combines these two policies, i.e. it derives from the two policies
applied on itself (see Listing 8).
template <template <class> class DataPolicy |
Listing 8 |
The requirements for the policy any DataPolicyX is to define a type VarTypeInit to be used in its constructor and have a method a_impl that returns a type that can be converted to const double & . Likewise for UpdatePolicyX which must have a method update_impl (no return type is required).
Now, we can simply define Neuron1 as a combination of Data1 and UpdateMech1 :
typedef Neuron<Data1, UpdateMech1> Neuron1;
The policies can communicate together, via the casting back to TypeImpl . Indeed, thanks to the friend declarations in Neuron , each policy can access to the members of Neuron , which inherits the protected members of all the policies. Note also that the public function members of the policies and of Neuron will be public for TypeImpl . What is private within a policy cannot be accessed by other policies, to keep safe variables (e.g. via protected accessors). An alternative option is to use protected derivation instead of the public one here in the design of Neuron , in order to prevent public members of the policies from being accessible from TypeImpl .
Now, we define two new policies
Data2
and
UpdateMech2
(respectively in the same family as
Data1
and
UpdateMech1
), but
the data
a_
is no longer constant in
Data2
, and the update method of
UpdateMech2
changes its value. Furthermore,
Data2
has a
learning
method which modifies its variable
a_
according to the variable
state_
(see Listing 9).
template <class TypeImpl> template <class TypeImpl> |
Listing 9 |
These two policies are 'compatible' in the sense that the update policy modifies the data a_ , which is not constant as a return type of a_impl in Data2 and the type Neuron2 defined by:
typedef Neuron<Data2, UpdateMech2> Neuron2;
Neuron2 nrn2;
nrn2.update();
is thus 'legal'. So is the combination between Data2 and UpdateMech1 .
However, the class Neuron3 defined here:
typedef Neuron<Data1, UpdateMech2> Neuron3;
Neuron3 nrn3;
nrn3.update();
is ill-defined because the implemented update_impl method of UpdateMech2 tries to modify the data a_ of Data1 which is constant, and the method cannot be called on an object of this type. Such an incompatibility is detected at compile time (the member function templates described achieve same functionalities). Depending on the compiler, the incompatibility is detected when defining the type Neuron<Data1, UpdateMech2> (g++) or when calling the method update (VC8).
The term bridged objects here refers to instantiated types using a policy bridge. If we want to design an interface for all the neurons to access a_impl (with const double & as common return type) and update_impl , we can add another derivation to Neuron with pure virtual methods. For example for update_impl :
struct InterfaceBase
{
virtual void update() = 0;
};
which provides the common method update that has to be defined in a derived class. We can either do it in Neuron (or in a derived class if needed). Defining it in Neuron saves some code:
template <...> class Neuron : public InterfaceBase,
...;
void Neuron<...>::update() {
UpdatePolicy<Neuron>::update_impl();}
but then the order of the policies in the list of the template arguments is then fixed. On the contrary, in a dedicated derivation (instead of a typedef like Neuron1 above), the order of the template argument list can be changed at will:
class Neuron4
: public Neuron<Data1, UpdateMech1>
, public InterfaceBase;
and we can also imagine combining more than two policies to define neurons. The latter option would keep the design more generic (see the generic version PolicyBridgeN of Neuron ), with no member function but the constructor. Yet, the same code would be duplicated in all the derived classes. Note that such an interface also allows us to store neurons in standard containers.
Now, to implement a special interface for the 'learning' neurons, we just have to derive the suitable policy (only Data2 here, but we can imagine more) from another abstract base class with a pure virtual method called learning :
struct Interface1
{
Interface1() {list_learning_nrn.push_back(this);}
virtual void learning() = 0;
std::list<Interface1 * const>
list_learning_neurons;
};
template <class TypeImpl>
class Data2 : public Interface1 {...};
Thus, learning neurons can be handled and updated specifically (for their learning mechanisms only) separately from the rest of the neurons (useful when the distinct processes happen at distinct times for example):
for(std::list<Interface1 * const>::iterator i =
Interface1::list_learning_neurons.begin();
i != Interface1::list_learning_neurons.end();
++i)
(*i)->learning();
This polymorphic feature only uses the derivation from an abstract base class and the cost is the one of a virtual function call at run time. Note that any bridged class (whatever the bridge like Neuron ) can actually share the same interface, and thus can be handled together as shown here. An alternative solution could be to use the variant pattern and suitable visitors [ Boost ], [ Tiny ].
Further considerations
So far, apart from the dedicated interfaces, this design based on policies has functionality comparable with member function templates. Another difference is that the body of Neuron does not have to be changed for a new mechanism with a method that requires internal communication (like when adding Data2::learning ). We indeed do not need to change Neuron with policy, whereas we would have to define in the body of Neuron a call for Data2::learning<TypeImpl> when using member function templates.
Note that the computation speed is equivalent for the two options, since all the internal mechanisms are based on function calls (no virtual function, only the use of an interface involves a virtual function call).
Further refinement can be obtained using meta-functions (that implement polymorphism at compile-time). So far, we only considered policies with one sole template argument, which is eventually the type of the bridged object ( TypeImpl ). Say we want to parametrise some of the policies with more template parameters. For example, the learning in data class Data2 would depend on an algorithm embedded in a class, thus we need to add an extra template parameter for Data2 (and not Data1 ):
template<class TypeImpl, class Algo> class Data2;
We can no longer use the policy bridge to build neurons with distinct algorithms because the template signature of Data2 is different now (one template parameter in the original design of the policy bridge Neuron ). Meta-functions can help here:
template<class Algo>
struct GetData2
{
template <class TypeImpl>
struct apply
{
typedef Data1<TypeImpl, Algo> Type;
};
};
With this design,
TypeImpl
will be provided
by the bridge, and all the other template parameters can be set via
GetData2
(with suitable defaulting if needed). The
policy bridge then becomes as shown in Listing 10.
template <class GetData, class GetUpdateMech> |
Listing 10 |
Note that the instantiated policy GetData::template apply<Neuron>::Type was renamed into InstData using typedef (likewise for InstUpdateMech ) in order to simplify the code. A neuron type can thus be simply created:
typedef Neuron<GetData2<Algo1>,
GetUpdateMech2> Neuron5;
Non-template classes with member function templates can be used with usual visitors because they have a plain type. We adapt here the processing using basic visitors [ Alexandrescu01 ] (with an abstract base class VisitorBase ) to be able to visit the policies which a bridged object is made of:
class VisitorBase
{
virtual ~VisitorBase() {}
};
template <class T>
class Visitor
{
virtual void visit(T &) = 0;
};
In particular, such a visitor must have the same behaviour for all the types T = Policy1<...> , for a given Policy1 . We thus need a tag to identify each policy, which is common to all the instantiated types related to the same policy, and a solution is to use Policy<void> because it will never be used in any bridged object. A particular visitor for Data1 and Data2 would be implemented:
class DataVisitor
: public VisitorBase
, public Visitor<Data1<void> >
, public Visitor<Data2<void, void> >
{
void visit(Data1<void> & d) {...}
visit(Data2<void, void> & d) {...}
};
Now, a method apply_vis(VisitorBase & vis) has to be defined at the interface level (pure virtual method), and defined at the same level as update to call the method in the policy (here in Neuron1 , likewise for Neuron2 ):
void Neuron1::apply_vis(VisitorBase & vis)
{
InstData::apply_impl(vis);
}
Finally, the 'visitable part' of the policy
Data1
(the data that the visitor wants to access,
a_
here) has to be moved in the specialised instantiation
Data1<void>
from which derive all the
Data1<TypeImpl>
.
The function
apply_impl
has also to be defined
in its body (here with a solution using
dynamic_cast
to check the compatibility of the visitor), as shown in Listing 11.
template <class TypeImpl> class Data1; template <> template <class TypeImpl> |
Listing 11 |
This way, the argument of the call of visit is the parent object of type Data1<void> instead of the object itself. The cost of the passage of the visitor is thus a dynamic_cast plus a virtual function call. In the case the visitor is not compatible, nothing is done here, but an error could be thrown or returned by the method apply_impl . Likewise with Data2<void, void> for the visitable part of Data2 .
Policy Bridge design for an arbitrary number of policies
In the end, the code can be abstracted to bridge N policies (the
dedicated methods such as
update
, etc. are
'decentralised' in derived classes), in Listing 12 with N = 15.
template <class GetPolicy0 friend InstPolicy0; typedef typename InstPolicy0::TypeInit TypeInit0; PolicyBridge15(TypeInit0 var_init0 |
Listing 12 |
This design requires each policy to define a type
TypeInit
,
which is set to a 'fake void' type
NullType
when
no initialisation variable is required (same
NullType
as used for
TypeList
in [
Alexandrescu01
]). The
overriding of the pure virtual functions defined in the interface are
left to the implementation of the instantiated types here. A class
Neuron1
is derived from a suitable instantiation as
shown in Listing 13.
class Neuron1 |
Listing 13 |
A dedicated version of
PolicyBridge2
could
be written to create neurons with the common interface hard-wired:
InterfaceBase
should be put in the template argument
list and the methods
update
,
a
and
apply
can be centralised in the
PolicyBridge2bis
code (to save some code lines),
shown in Listing 14.
template <...> |
Listing 14 |
The code of PolicyBridgeN can be generated for all values of N in a define range using preprocessor macros such as REPEAT (cf. boost::preprocessor [ Boost ], the TTL library [ Tiny ]), i.e. to create the class templates PolicyBridge1 up to PolicyBridgeN , where N is an arbitrary limit.
See http://www.bionicear.org/people/gilsonm/prog.html for details on this implementation.
Conclusion
The policy bridge pattern aims to build classes that inherit 'properties' or functionalities from a certain number of policies (variable and function members from the protected 'space'). Each policy can call members from other ones and the compatibility between the policies is checked at compile time. This approach is modular and flexible, and keeps the design of policies related to distinct functionalities somehow 'orthogonal'.
A common interface can be designed in order to provide a main 'gate' (to all the common functionalities that have to be public, and the cost is the one of a virtual-function call), to store them in standard containers, etc. Moreover, specific interfaces can similarly be design for particular functionalities belonging to a restrained number of mechanisms, allowing us to pilot the bridged objects according to what they are actually made of. Usual visitors can be adapted and applied to these objects in order to interact with some of the policies involved in the design. Meta-functions can help and bring further refinement to use policies with more than one template parameter.
Such design proved to be useful and efficient in a simulation program where objects of various types interact: for instance neurons with distinct intern mechanisms, as well as diverse synapses to connect them; these objects are created at the beginning of the program execution and they evolve and interact altogether during the simulation. Requirements such as 'fast' object creation or deletion have not been considered here so far, 'optimisation' was sought for only for the function calls (call via the common interface class). Visitors were used for adequate object construction and linking objects from distinct kind (in particular between synapses and neurons to check compatibility when creating a synaptic connection), but not used during the execution: the interface then provides a faster way to access the functionalities of the objects.
Code is available online at
http://www.bionicear.org/people/gilsonm/index.html
to illustrate how
such a design can be used.
Acknowledgements
The author thanks Nicolas di Cesare for earlier discussion about design pattern, and Alan Griffiths, Phil Bass and Roger Orr for very helpful comments that brought significant improvements of the text during the review process. MG is funded by a scholarship from the NICTA Victorian Research Lab ( www.nicta.com.au ).
References
[ Alexandrescu01 ] A. Alexandrescu. Modern C++ design: generic programming and design patterns applied , Addison-Wesley, Boston, MA: 001. Includes bibliographical references and index.
[ Vandevoorde/Josuttis03 ] David Vandevoorde, Nicolai M. Josuttis C++ templates : the complete guide , Addison Wesley, 2003.
[ Boost ] Boost c++ libraries. http://www.boost.org/
[ Tiny ] Tiny Template Library. http://tinytl.sourceforge.net/