Providing Initialization Methods
When you provide an initialization method (an approach described by Smaragdakis and Batory), you depart from the common C++ practice in that an object has to be properly initialized by executing one of its constructors. Either no constructor is defined which implies the generation of a default constructor by the compiler or a standard constructor is implemented that initializes the class members with some reasonable default values. The actual initialization is left to special initialization methods that assign their argument values to the class members (see Listing Four).
Listing Four
#include <iostream> using namespace std; // Define special intialization methods in each class and no longer rely // on the proper initialization though constructors. class Customer { public: // Initialization method for Customer. // A default constructor will be generated automatically. void init(const char* fn,const char* ln) { firstname_ = fn; lastname_ = ln; } void print() const { cout << firstname_ << ' ' << lastname_; } private: const char *firstname_, *lastname_; }; template <class Base> class PhoneContact: public Base { public: // Initialization method for PhoneContact only. // A default constructor will be generated automatically. void init(const char* pn) { phone_ = pn; } void print() const { Base::print(); cout << ' ' << phone_; } private: const char *phone_; }; template <class Base> class EmailContact: public Base { public: // Initialization method for EmailContact only. // A default constructor will be generated automatically. void init(const char* e) { email_ = e; } void print() const { Base::print(); cout << ' ' << email_; } private: const char *email_; }; int main() { // Compiler generated default constructor gets called. Customer c1; // Now explicitly invoke the initialization method. c1.init("Teddy","Bear"); c1.print(); cout << endl; // Basically the same as above. PhoneContact<Customer> c2; // But initialization method for Customer must also be explicitly invoked! c2.Customer::init("Rick","Racoon"); c2.init("050-998877"); c2.print(); cout << endl; // Basically the same as above. EmailContact<Customer> c3; c3.Customer::init("Dick","Deer"); c3.init("[email protected]"); c3.print(); cout << endl; // Now the three initialization methods of three different mixin classes // must be explicitly invoked! The composed class does not provide its own // initialization method. EmailContact<PhoneContact<Customer> > c4; c4.Customer::init("Eddy","Eagle"); c4.PhoneContact<Customer>::init("[email protected]"); c4.EmailContact<PhoneContact<Customer> >::init("049-554433"); c4.print(); cout << endl; // Basically the same as above. PhoneContact<EmailContact<Customer> > c5; c5.Customer::init("Eddy","Eagle"); c5.EmailContact<Customer>::init("[email protected]"); c5.PhoneContact<EmailContact<Customer> >::init("049-554433"); c5.print(); cout << endl; return 0; }
This approach is error-prone because the client programmer is responsible for calling the necessary initialization methods in the correct order. This is important because one cannot generally assume that the initialization of the members of derived classes never depends on the base class members. Furthermore, inherited initialization methods must be invoked using explicit qualification syntax, which is awkward.
Defining Additional Constructors
When you define additional constructors, you make use of the template instantiation mechanism of C++. It is important to know that only those parts of a template class that are actually used get instantiated. Consequently, it is possible that partial instantiations of a given template class will be legal, even when a full instantiation is not. This lets you define the different constructors in mixin classes for the different possible base classes; see Listing Five.
Listing Five
#include <iostream> using namespace std; // Define additional constructors that will be instantiated only if required. class Customer { public: Customer(const char* fn,const char* ln):firstname_(fn),lastname_(ln) {} void print() const { cout << firstname_ << ' ' << lastname_; } private: const char *firstname_, *lastname_; }; template <class Base> class PhoneContact: public Base { public: // The following constructors will be instantiated only if required. PhoneContact( const char* fn,const char* ln, const char* pn):Base(fn,ln),phone_(pn) {} PhoneContact( const char* fn,const char* ln, const char* pn,const char* e) :Base(fn,ln,e),phone_(pn) {} void print() const { Base::print(); cout << ' ' << phone_; } private: const char *phone_; }; template <class Base> class EmailContact: public Base { public: // The following constructors will be instantiated only if required. EmailContact( const char* fn, const char* ln, const char* e):Base(fn,ln),email_(e) {} EmailContact( const char* fn,const char* ln, const char* pn,const char* e) :Base(fn,ln,pn),email_(e) {} void print() const { Base::print(); cout << ' ' << email_; } private: const char *email_; }; int main() { Customer c1("Teddy","Bear"); c1.print(); cout << endl; PhoneContact<Customer> c2("Rick","Racoon","050-998877"); c2.print(); cout << endl; EmailContact<Customer> c3("Dick","Deer","[email protected]"); c3.print(); cout << endl; EmailContact<PhoneContact<Customer> > c4("Eddy","Eagle","049-554433","[email protected]"); c4.print(); cout << endl; PhoneContact<EmailContact<Customer> > // The following composition prints the last two arguments in reverse // order because the print() method is composed differently than previously. c5("Eddy","Eagle","049-554433","[email protected]"); c5.print(); cout << endl; return 0; }
At first glance, this approach might look attractive. But after introducing a new mixin class with special arguments for its initialization, additional constructors would have to be implemented in all dependent classes. Depending on the number of possible compositions of mixin classes, this could result in a maintenance nightmare. Please note that, as already described, composition of the same mixin classes in a different order may lead to different behaviors; for instance, PhoneContact<EmailContact<Customer> >
and EmailContact<PhoneContact<Customer> >
lead to different implementations of the print()
method. Thus, the number of different compositions may grow more than exponentially.
Designing an Advanced Solution
The previously described approaches of providing a special argument class and defining additional constructors provide the basic framework for designing the more advanced solution.
First, it is a good idea to provide a special class that wraps the constructor arguments and thus provides a uniform constructor interface. What should be achieved then is the provision of different argument wrappers for different compositions of mixin classes. We will see how to solve this problem by applying some template-metaprogramming techniques such as described in our book Generative Programming: Methods, Tools, and Applications (Addison-Wesley, 2000; see http://www.generative-programming.org/).
Second, the client programmer should be able to adhere to the usual way of declaring objects without the obscuring and awkward argument wrapper construction syntax. Thanks to member templates, highly generic constructors can be implemented that take ordinarily specified arguments and automatically convert them to instances of the argument wrapper classes.