Heterogeneous Value Lists
The object model of C++ requires that an object is constructed by calling the constructor of its topmost base(s), then the immediately descendant class(es), and so on, until the constructor of the most-derived class is executed. Therefore, the set of constructor arguments of a base class is generally a subset of the constructor arguments of any derived class. Imagine passing a singly linked list containing all arguments instead of separate arguments. Each constructor could then take the elements from the front of the list that it needs as arguments and pass the remainder of the list to the base class constructor. This process goes on until the last element has been taken away and the list is empty. Unfortunately, this is not so easy in C++ because each list element may have a different type and, consequently, each list node also represents a unique type. Hence, a highly flexible list is required that links unique node types, each with a possibly different type for storing a value.
The basis for a solution provide compile-time type lists and recursively synthesized types (such as described in "Synthesizing Objects," by K. Czarnecki and U.W. Eisenecker, Proceeding of the 12th European Conference on Object-Oriented Programming, Springer Verlag, 1999, and http://www.prakinf.tu-ilmenau.de/~czarn/cpe2000). A type list is a singly linked list constructed by recursively instantiating a structure template such as the following template List
:
struct NIL {}; template<class Head_, class Tail_ = NIL> struct List { typedef Head_ Head; typedef Tail_ Tail; };
A list of basic signed integral types can be represented as follows:
List<signed char,List<short,List<int,List <long> > > >
To represent a list of arguments of any type and number, we need a type list that additionally stores values. Such a list of different types and values is called a "heterogeneous value list" (for more information, see http://www.tucs.abo.fi/publications/techreports/TR249.html). Listing Six is a heterogeneous value list for representing argument lists.
Listing Six
#include <iostream> using namespace std; // We need NIL because - as opposed to void - it must be possible // to create instances of it. struct NIL {}; template <class T,class Next_ = NIL> struct Param { Param(const T& t_,const Next_& n_ = NIL()):t(t_),n(n_) {} const T& t; Next_ n; typedef Next_ N; }; struct SomePersonParameters { const char *firstname_, *lastname_; int age_; SomePersonParameters(const char* fn,const char* ln,const int age) :firstname_(fn),lastname_(ln),age_(age) {} }; int main() { SomePersonParameters p1("Peter","Parrot",3); cout << p1.firstname_ << ' ' << p1.lastname_ << ' ' << p1.age_ << endl; // Can be rewritten as Param<const char*,Param<const char*,Param<int> > > p2("Peter",Param<const char*,Param<int> >("Parrot",3)); //please note //that we can pass 3 as the last element instead of Param<int>(3) //because it will be automatically converted using the constructor //of Param cout << p2.t << ' ' << p2.n.t << ' ' << p2.n.n.t << endl; // Or more easily readable typedef Param<int> T1; typedef Param<const char*,T1> T2; typedef Param<const char*,T2> T3; T3 p3("Peter",T2("Parrot",3)); cout << p3.t << ' ' << p3.n.t << ' ' << p3.n.n.t << endl; return 0; }
Listing Six also demonstrates how to use the Param
template. Appropriate typedef
s slightly simplify the declaration of a parameter type and its instances. Accessing the nodes and the remainder of a list is straightforward. Because Param
has a type member N
referring to the type of the remainder of the list, the latter can be easily accessed as SomeRemainder::N
. Whereas the manual definition of recursive heterogeneous value lists is remarkably awkward, it is easy to automatically compute them.
Configuration Repositories and Parameter Adapters
The next step is more complex because two different techniques have to be appropriately combined. The first technique is to use traits classes (see http://www.cantrip.org/traits.html) as configuration repositories. Configuration repositories let you separate the configuration aspect from the implementation of a component; that is, a mixin class. The only parameter of a base mixin class is then the configuration repository (see Customer
in Listing Seven). The base mixin class exports the configuration repository by defining an alias name as its member type. As a result, if the instantiated mixin class is used itself as a base class parameter, the descendant mixin class can read out this configuration repository and export it again (see PhoneContact
and EmailContact
in Listing Seven). This way, the configuration repository can be propagated along the inheritance hierarchy. Each mixin class can retrieve any desired information from the configuration repository.
Listing Seven
#include <iostream> #include <string> using namespace std; struct NIL {}; template <class T,class Next_ = NIL> struct Param { Param(const T& t_,const Next_& n_ = NIL()):t(t_),n(n_) {} const T& t; Next_ n; typedef Next_ N; }; template <class Config_> class Customer { public: // Exporting config typedef Config_ Config; // Create parameter type typedef Param< typename Config::LastnameType, Param< typename Config::FirstnameType > > ParamType; Customer(const ParamType& p) :lastname_(p.t),firstname_(p.n.t) {} void print() const { cout << firstname_ << ' ' << lastname_; } private: typename Config::FirstnameType firstname_; typename Config::LastnameType lastname_; }; template <class Base> class PhoneContact: public Base { public: // retrieve config and export it typedef typename Base::Config Config; // retrieve the constructor parameter type from the base class // and extend it with own parameters typedef Param< typename Config::PhoneNoType, typename Base::ParamType > ParamType; PhoneContact(const ParamType& p) :Base(p.n),phone_(p.t) {} void print() const { Base::print(); cout << ' ' << phone_; } private: typename Config::PhoneNoType phone_; }; template <class Base> class EmailContact: public Base { public: // retrieve config and export it typedef typename Base::Config Config; // retrieve the constructor parameter type from the base class // and extend it with own parameters typedef Param< typename Config::EmailAddressType, typename Base::ParamType> ParamType; EmailContact(const ParamType& p) :Base(p.n),email_(p.t) {} void print() const { Base::print(); cout << ' ' << email_; } private: typename Config::EmailAddressType email_; }; template <class Base> struct ParameterAdapter: Base { // Retrieve config from Base and export it. typedef typename Base::Config Config; // Retrieve the most complete param type typedef typename Config::RET::ParamType P; typedef typename P::N P1; typedef typename P1::N P2; // Constructor adapter with 1 argument template < class A1 > ParameterAdapter( const A1& a1) :Base(a1) {} // Constructor adapter with 2 arguments template < class A1, class A2 > ParameterAdapter( const A1& a1, const A2& a2) :Base(P(a2,a1)) {} // Constructor adapter with 3 arguments template < class A1, class A2, class A3 > ParameterAdapter( const A1& a1, const A2& a2, const A3& a3) :Base(P(a3,P1(a2,a1))) {} // Constructor adapter with 4 arguments template < class A1, class A2, class A3, class A4 > ParameterAdapter( const A1& a1, const A2& a2, const A3& a3, const A4& a4) :Base(P(a4,P1(a3,P2(a2,a1)))) {} }; struct C1 { // Provide standard name for config typedef C1 ThisConfig; // Provide elementary types typedef const char* FirstnameType; typedef const char* LastnameType; // Parameterize base class typedef Customer<ThisConfig> CustomerType; // Add ParameterAdapter typedef ParameterAdapter<CustomerType> RET; }; struct C2 { // Provide standard name for config typedef C2 ThisConfig; // Provide elementary types typedef const char* FirstnameType; typedef const char* LastnameType; typedef const char* PhoneNoType; // Assemble mixin classes typedef Customer<ThisConfig> CustomerType; typedef PhoneContact<CustomerType> PhoneContactType; // Add ParameterAdapter typedef ParameterAdapter<PhoneContactType> RET; }; struct C3 { // Provide standard name for config typedef C3 ThisConfig; // Provide elementary types typedef const char* FirstnameType; typedef const char* LastnameType; typedef const char* EmailAddressType; // Assemble mixin classes typedef Customer<ThisConfig> CustomerType; typedef EmailContact<CustomerType> EmailContactType; // Add ParameterAdapter typedef ParameterAdapter<EmailContactType> RET; }; struct C4 { // Provide standard name for config typedef C4 ThisConfig; // Provide elementary types typedef const char* FirstnameType; typedef const char* LastnameType; typedef const char* PhoneNoType; typedef const char* EmailAddressType; // Assemble mixin classes typedef Customer<ThisConfig> CustomerType; typedef PhoneContact<CustomerType> PhoneContactType; typedef EmailContact<PhoneContactType> EmailContactType; // Add ParameterAdapter typedef ParameterAdapter<EmailContactType> RET; }; int main() { C1::RET c1("Teddy","Bear"); c1.print(); cout << endl; C2::RET c2("Rick","Racoon","050-998877"); c2.print(); cout << endl; C3::RET c3("Dick","Deer","[email protected]"); c3.print(); cout << endl; C4::RET c4("Eddy","Eagle","049-554433","[email protected]"); c4.print(); cout << endl; return 0; }
A configuration repository can contain any type information; for example, types needed by a mixin other than its base class (so-called "horizontal parameters"), static constants, and enumeration constants. Furthermore, it can also contain any type that is being composed, even the final type of the complete composition. The configuration repository idiom is inherently recursive because it parameterizes base mixin classes with itself. Listing Seven contains four sample configuration repositories C1
, C2
, C3
, and C4
. They are designed to represent the four possible compositions that are also part of the previously mentioned examples. To demonstrate the ability to read out horizontal parameters, the types of first name, last name, phone number, and e-mail address are also parameterized and defined in the configuration repositories and retrieved by the mixin classes. In our example, the components assemble the needed heterogeneous parameter lists themselves rather than retrieving them from the configuration repository. This is because the assembly always follows the same pattern, so that it can be safely performed locally in the components. Consequently, the resulting code is simpler than it would be with heterogeneous parameter lists assembled in the configuration repository.
The other technique to use is a highly generic parameter adapter (see ParameterAdapter
in Listing Seven) that accepts any number of constructor arguments of any type and converts them into a singly linked list of types and arguments. The wrapper is a mixin class itself and is expected to be used as the most-derived mixin in a composition. Scott Meyers wrote about the use of member templates to allow method signatures with arguments of any type and number (see "Implementing operator->*
for Smart Pointers," DDJ, October 1999; http://aristeia.com/Papers/DDJ_Oct_1999.pdf), as has V. Batov in "Safe and Economical Reference-Counting in C++" (C/C++ Users Journal, June 2000). However, to our knowledge, a generic parameter adapter combining this technique with heterogenous argument lists has not been documented elsewhere.
The construction of the parameter adapter is amazingly simple. The adapter includes one generic constructor as a member template for a given number of arguments. In this example, we provided template member functions covering constructors with up to four arguments (Listing Seven). In practice, it would be useful to define a few additional generic constructors, let's say, for up to 15 or 20 arguments. Implementing these template member functions is obvious and easy. Note that this is a truly universal parameter adapter, which will work for any mixin classes that accept heterogeneous value lists as arguments of their constructors. By the way, if the most-derived mixin class is initialized by a standard constructor, the parameter adapter has to be omitted, of course. Finally, the constructors of the mixin classes have to be adjusted for accepting heterogeneous value lists as arguments.