Registration and Prototypes

Abstract

There are many situations in which it is convenient to store all objects of a certain class, or at least a certain subset of those objects in a registry. We dicuss this.

A registry itself is easy to define. It's typically either a static member function or a global function with the following form:


list<myClass*>& getList()
{
	static list<myClass*> x;
	return x;
}

Objects usually register themselves via a constructor. For example, we could use something like this:

myClass::myClass () 
{
	getList().push_back(this);
	// put other code here.
}

So you may wonder, why is the list ``hidden'' inside a function, why not just make it a global. Well, let's try it.

	// Do NOT do this !
	list<myClass*> theList; // global !

In another translation unit, we'll have a global object (or static data member) that registers itself.

	myClass myObject;
The problem is, we don't know if the constructor for myObject is called before or after the list is created. Order of initialisation across translation units is undefined. The only thing that is well defined is order of initialisation within a translation unit. If the constructor is called before the list is ready, the program will crash, most probably with a very cryptic error message. On the other hand, when we wrap the list in a function, we are assured that the list will be created when it is needed.

Lazy versus Aggressive Initialisation

For the purpose of the following discussion, we call the initialisation of global objects aggressive initialisation, and initialisation of objects that are ``firewalled'' behind a function (like our list) lazy initialisation.

The terminology is standard but general, so the reader is warned that ``lazy/agressive initialisation'' is an ambiguous term unless a definition like that above is provided. Note that laziness is a general term that refers to doing something when it is required and no sooner.

We briefly summarise their characteristics:

Aggressive Initialisation

Lazy Initialisation

An Application: Prototypes

Suppose we want a flexible way to defer choices about object creation until runtime, and we want to avoid hard-coding a list of class names in the base class. This may seem like a minor gain at first, but it has the following advantages:

So let's write some code! First we have a base class:

class Dummy 
{
public:
    Dummy(std::string s) : s(s){}
	std::string message()
	{ 
		return s; 
	}
private:
	std::string s;
};

class Base
{

    Base(Dummy) 
    {
        // do some initialisation.
        prototypeList().push_back(this);
    }

    virtual Base* make ( std::string filename );
    virtual Base* clone() 
	{ 
	    return 0; 	
	}
    virtual bool isMine ( std::string ) { return false; }
    static list<Base*>& prototypeList()
    {
        static list<Base*> foo;
        return foo;
    }
    virtual ~Base(){}
protected:
    Base();
    Base(const Base&);
    Base& operator=(const Base& );
};

The make method in the derived class is very simple, it looks like this:

MyNameSpace::Base*
MyNameSpace::Derived::make(std::string filename) 
{
    return new Derived;
}

We also need to implement isMine() in the derived class or it will never get created. For example, if the string is a filename, and we're interested in creating an object of type Derived if and only if the filename's extension is .img, we'd implement isMine() as follows:

bool
MyNameSpace::Derived::isMine(std::string filename)
{
    int a = filename.find_last_of (".");
    if ( a == -1 )
        return false;

    if ( filename.compare(".img", a,  4 ))
        return true;
    else
        return false;
}

Now we get to the important part -- the make method in Base class.

MyNamespace::Base* 
MyNamespace::Base::make ( std::string filename )
{
    std::list<Base*>::iterator it = prototypeList().begin();
    for 
    (; it != prototypeList().end(); ++it;)
    {
        if ( (*it)->isMine ( filename ) )
            (*it)->make(filename);
    }
}

There is a potential gotcha in this make() method: the loop can very easily be infinite if you're not careful, because it's possible that the call to make is called on a base class prototype. The way to make sure this does not happen is to check that the base class version of isMine() always returns false.

The last step is to write registration constructors and declare the protypes.

MyNamespace::Base BasePrototype(Dummy("Base"));
MyNamespace::Derived DerivedPrototype(Dummy("Derived"));
We should add appropriate methods to Base class, but this is a start. The example is basically boiler-plate code. In a real example, most likely you will have several other methods.

Polymorphic Prototypes

It's preferable to invoke the prototypes via pointers to get polymorphic behaviour. To do this, we create a global object in an unnamed namespace. For example, we could do this in derived.cc

namespace 
{	
	Derived Prototype(Dummy("Derived"));
}
MyNamespace::DerivedPrototype* = &Prototype;

The anonymous namespace (now there's an oxymoron!) is visible inside the translation unit containing derived.cc, but not elsewhere. Note that DerivedPrototype should be declared in the header derived.h, but Prototype should not be.

Putting it All Together

base.h


#ifndef BASE_H
#define BASE_H
#include <registration.h>
#include <string>
#include <list>
namespace MyNamespace 
{

class Base 
{
public:
	Base(const Registration&);
	virtual Base* make (std::string filename);
	virtual Base* clone();
	virtual bool isMine(std::string);
	virtual ~Base();
protected:
	Base();
	Base(const Base&);
	Base& operator=(const Base&);
};

list<Base*>& prototypeList();
extern Base* God;

} //namespace

#endif

base.cc


#include <base.h>


using namespace MyNamespace;

Base::Base(const Registration&)
{
	prototypeList().push_back(this);
}

Base* Base::make (std::string filename)
{
	std::list<Base*>::iterator it = prototypeList().begin();
	for ( ; it!=prototypeList().end(); ++it )
	{
		if ((*it)->isMine(filename))
			(*it)->make(filename);
	}
}

Base* Base::clone() 
{
	return new Base;
}

bool Base::isMine(std::string)
{
	return false;
}

Base::~Base() {}

// non-members

list<Base*>& prototypeList()
{
	static list<Base*> foo;
	return foo;
}

namespace
{
	Base Prototype(Registration("Base"));
}
MyNamespace::Base* God = &Prototype;

//-------------------      PRIVATE        -----------------------------
Base::Base(){}
Base::Base(const Base&){}
Base& Base::operator=(const Base&){ return *this; }


registration.h


#ifndef REGISTRATION_H
#define REGISTRATION_H
#include <string>
namespace MyNamespace
{
class Registration
{
public:
	Registration(std::string s) : s(s) {}
	std::string message()
	{
		return s;
	}
private:
	std::string s;
};

}//namespace

#endif




derived.h


#ifndef DERIVED_H
#define DERIVED_H
#include <base.h>
#include <string>
#include <list>
namespace MyNamespace 
{

class Derived : public Base
{
public:
	Derived(const Registration&);
	virtual Base* make (std::string filename);
	virtual Base* clone();
	virtual bool isMine(std::string);
	virtual ~Derived();
protected:
	Derived();
	Derived(const Derived&);
	Derived& operator=(const Derived&);
};

extern Derived* DerivedPrototype;

} //namespace

#endif

derived.cc


#include <derived.h>


using namespace MyNamespace;

Derived::Derived(const Registration&)
{
	prototypeList().push_back(this);
}

Base* Derived::make (std::string filename)
{
	std::list<Base*>::iterator it = prototypeList().begin();
	for ( ; it!=prototypeList().end(); ++it )
	{
		if ((*it)->isMine(filename))
			(*it)->make(filename);
	}
}

Base* Derived::clone() 
{
	return new Derived;
}

bool Derived::isMine(std::string filename)
{
	// ----- this is just an example. replace it as you see fit.
	int a = filename.find_last_of(".");
	if ( a == -1 )
		return false;
	if (filename.compare(".img", a, 4))
		return true;
	return false;
	// ---------------------------------------------------------
}

Derived::~Derived() {}

// non-members



namespace
{
	Derived Prototype(Registration("Derived"));
}
MyNamespace::Derived* DerivedPrototype = &Prototype;