Generic Programming is a simple ideal. Generic Programming is the art of taking a set of common code and generalising an abstraction that performs the general case and which can be easily specialised to handle unusual cases.
The best examples of Generic Programming are Design Patterns. Design Patterns are a general solution to a set of problems commonly encountered in computer programs. The definitive text is "Design Patterns: Elements of Reusable Object-Oriented Software." This text defines the solution to many architecture problems, such as The Singleton - A class with only one instance e.g. BApplication, or Iterators - Sequential access to a collection e.g. BEntryList::GetNextEntry()
Design patterns are a solution; there is no code for you to use.
While reading many journal articles on the STL and generic programming, I realised I didn't understand any of it. When I read the explanation of Patterns and Iterators in particular Generic Programming began to make sense.
Generic Programming is about not writing code. We hate rewriting code; it is time consuming and prevents us from doing the work we want. Generic programming is simply abstracting the common general solution from a set of them. To use generic code you only need to specialise obscure details. "Modern C++ Design: Generic Programming and Design Patterns Applied" shows how this can be done, and I hope to introduce these ideas to you. FYI: This book discusses how to use the generic capabilities of C++ to implement your design. The author provides an implementation of The Singleton, Functors and other patterns.
I recommend you read both these books, although you might want to brush up on basic templates before reading "Modern C++ Design" and this article.
Patterns of BeOS
Beginning an application for BeOS is not difficult. The applications mostly follow a single pattern.
Define the type of the main window, this will inherit BWindow.
Define the type of the application, this will inherit BApplication.
Define the applications constructor to create a main window.
Define the applications run function to show the created window.
Define the main routine to create an instance of the application
then run that application.
The general case varies only on the type of main window to create and the application signature. To get a clearer idea lets look at the code for a sample application.
class MBApplication : public BApplication {
typedef BApplication super;
public:
MBApplication( void )
: super( SIGNATURE_VALUE )
{
the_win = new WINDOW_TYPE();
}
// No destructor as it crashes app.
virtual void ReadyToRun(void) {
the_win->Show();
}
protected:
WINDOW_TYPE * the_win;
};
int main( void ) {
be_app = NULL;
int errorlevel = 0;
try {
// Automatically assigned to be_app
new MBApplication( );
if( B_OK != be_app->InitCheck() ) {
errorlevel = APP_INIT_FAIL_VALUE;
} else {
be_app->Run();
errorlevel = 0;
}
} catch( ... ) {
// Ensure children clean up.
delete be_app;
throw;
}
delete be_app;
return errorlevel;
}
Many applications only differ in the values of SIGNATURE_VALUE, INIT_FAIL_VALUE and the type WINDOW_TYPE. C programmers might define them as pre-processor macros, that's generally not a good solution. As C++ programmers, we have templates. A template is essentially a compiler variable. We can define these three values as template parameters of the class and function, T for template type and TV for template value.
template< class T_Window, char const * TV_SIGNATURE >
class MBApplication : public BApplication {
typedef BApplication super;
public:
MBApplication( void )
: super( TV_SIGNATURE )
{
the_win = new T_Window();
}
// No destructor as it crashes app.
virtual void ReadyToRun(void) {
the_win->Show();
}
private:
T_Window * the_win;
};
template< class T_Application, int TV_INIT_FAIL >
int MBMain( void ) {
be_app = NULL;
int errorlevel = 0;
try {
// Automatically assigned to be_app
new T_Application();
if( B_OK != be_app->InitCheck() ) {
errorlevel = TV_INIT_FAIL;
} else {
be_app->Run();
errorlevel = 0;
}
} catch( ... ) {
// Ensure children clean up.
delete be_app;
throw;
}
delete be_app;
return errorlevel;
}
Notice how little code changes to make it generic. There is still the issue of how to use this generic code. You will still need to have your Application Window and your main function. Except your main function now looks like this one.
int main( void ) {
return MBMain< MBApplication< AWindow, APP_SIG >, OOPS >();
}
This is syntax is called template instantiation. You are telling the compiler, you want this template entity created with these values. Above, you are creating a MBApplication
class that uses an AWindow
and has APP_SIG
as its signature. You then tell the compiler to create a MBMain
function that uses your application and returns OOPS
if it can't start. Generic code is shown here by the fact you can use the MBMain
function it with any application type. Even an application not derived from MBAppplication
. We will try to keep our function and class separate.
There are always problems
There is a problem with the above code: it restricts the signature and initialisation failure value to compile time constants. Also GCC on Be doesn't like that, any attempts to get them to play nicely result in internal compiler errors.
The solution to these problems are to consider that the signature of an application and its initialisation failure as characteristic traits of the class. Generic algorithms have to deal with widely varying types and usage patterns, often needing to vary themselves depending upon a class's traits. If your application had to run a special loading function before it could start you might want to disable the default constructor. As such the construction would be a trait of the class, it would be implemented as a call to new
for most classes and specialised for different classes.
Since the implementation of template classes can vary dependant upon their arguments, C++ allows you to specialise templates. That is, you can provide a special implementation for a certain type. The standard library has a specialised version for vectors of boolean values. This allows the vector to save space and compact a thirty two element vector from one hundred and twenty eight bytes down to four bytes, a 32:1 saving.
We declare a trait class as an external type that takes the type it describes the character for, as a template parameter. We then specialise it for our application. To specialise you copy the existing code you supply the parameter for the template. Please Note: that >> will be interpreted as shift by the compiler so remember to leave a space.
template< class T_Application >
class MBApplicationTraits {
public:
static inline char const * Signature( void ) {
return "application/x-vnd.unknown-mbapp.unknown";
}
static inline int InitFailureValue( void ) {
return 1;
}
};
template< >
class MBApplicationTraits< MBApplication< AWindow > > {
public:
static inline char const * Signature( void ) {
return "application/x-vnd.unknown-mbapp.unknown";
}
static inline int InitFailureValue( void ) {
return 1;
}
};
We remove the fixed parameters from the generic application and main routine above, replace the parameters with calls to the trait class.
template< class T_Window >
class MBApplication : public BApplication {
MBApplication( void )
: super( MBApplicationTraits < MBApplication < T_Window > >
::Signature() )
// ...
}
// And
template< class T_Application >
int MBMain( void ) {
// ...
if( B_OK != be_app->InitCheck() ) {
errorlevel = MBApplicationTraits< T_Application >
::InitFailureValue();
// ...
}
The above code works perfectly out of the box. A macro, MB_APPLICATION_TRAITS_M, is provided to make it easier to use. Now you only need the sample code, a window class and a reworked version of the following code to create a simple application for BeOS. You can sub-class the application class, with a fixed or template parameter, if you wish to augment it.
MB_APPLICATION_TRAITS_M(
MBApplication< AWindow >,
"application/x-vnd.mfh-mbapp.sample",
1 )
int main( void ) {
return MBMain< MBApplication< AWindow > >();
}
Generic Programming is about not writing code. Once you have written the code several times you can step back and see its general form. You can use templates to provide "out of the box" code to use. You can use the powerful idiom of traits to extract the type specific and design specific code and generalise your solution further still.
Wait there's more ...
The current trait class is limited by being unable to access any runtime data. The InitFailValue
function for example can not retrieve a value from the application class. It can use be_app
, but using global variables often indicates poor design and in this case, there is a better one.
If we pass a pointer to the application to both trait functions, the application can be accessed as a normal class. Remember the trait class is not a friend by default. If we pass each function a pointer and it is the pointers type that is the template parameter then we no longer need to have a class template. This is because we can make the functions' templates.
Not only does this give us cleaner access to class data but it also cleans our code up substantially. We no longer need to explicitly specify the trait class for the signature or the return value. Nor do we have to specialise the entire class. We can just specialise the trait functions we need. It is not much of a saving, but it is a good example of template functions. There is one caveat, however, we can not just pass be_app
to InitFailValue
we must cast it to the correct type.
class MBApplicationTraits {
public:
template< class T_Application >
static inline char const * Signature( T_Application * the_app ) {
return "application/x-vnd.unknown-mbapp.unknown";
}
template< class T_Application >
static inline int InitFailureValue( T_Application * the_app ) {
return 1;
}
};
template< class T_Window >
class MBApplication : public BApplication {
MBApplication( void )
: super( MBApplicationTraits::Signature( this ) )
// ...
};
// And
template< class T_Application >
int MBMain( void ) {
// ...
if( B_OK != be_app->InitCheck() ) {
// Cast or it will use the BApplication version
errorlevel = MBApplicationTraits::InitFailureValue(
static_cast< T_Application* >( be_app ) );
// ...
}
// Specialize traits for MBApplication< AWindow >
template< >
char const * MBApplicationTraits::
Signature< MBApplication< AWindow > >
( MBApplication< AWindow > * the_app ) \
{ return "application/x-vnd.mfh-mbapp.sample"; }
// The main stays the same.
int main( void ) {
return MBMain< MBApplication< AWindow > >();
}
Generalising the creation of the window and application will be dealt with in another article. There is a new concept called policies that perfectly capture that problem.
Source Code:
MBApplication.zip
Bibliography
Andrei Alexandrescu (1995)
"Modern C++ Design: Generic Programming and Design Patterns Applied."
Boston: Addison Wesley. ISBN: 0-201-70431-5.
WWW:
http://www.awl.com/cseng/titles/0-201-70431-5
Gamma, Erich, Richard Helm, Ralph Johnson, John Vlissides (1995)
"Design Patterns: Elements of Reusable Object-Oriented Software."
Reading, MA: Addison Wesley.