MIRA
Class Factory

Contents

This document acts as a manual. For further implementation details see Class Factory (Implementation Details).

What is the Class Factory?

When using C++ you may want to allocate dynamic objects from time to time. The common way to do this is to use the C++ keyword new. Although this approach is sufficient most of the time, it fails if you don't know the exact type of the desired object at compile time, for example if you want to create an instance of an object upon a condition which can only be evaluated during runtime (e.g. depending on user input). There are a lot of different approaches known from literature to solve this problem. Factory methods or static factories are just two examples of these so called Factory patterns. However, the basic idea is quite similar. Instead of using the Keyword new, a special factory function is called to create an object of the desired type. Since the desired type is not known during compile time, the factory function takes a class identifier (a string or a magic number) as argument and returns an instance of the associated class. In this process, the factory function is responsible for the mapping between the given identifier and the exact type information of the desired object. Therefore, the call of new may be replaced by something similar to Factory::newInstance( IDENTIFIER ). To support this code pattern, we have implemented a mixed static and abstract factory which has some similarities with the Object pattern known from Java.

Realization

The implemented concept basically consists of three different classes realizing the desired functionality.

Factory class

The Factory class is provided as a unique instance. Every class, which is derived (directly or indirectly) from the Object class is registered at the Factory. Therefore, the Factory can deliver class information of every child class of Object, which is known during runtime. Furthermore, it is capable to construct instances of the known classes. Since the Class objects themselves are responsible for creating the associated classes (as you will see later) the factory simply keeps track of every known Class object during runtime.

Object class

The Object class is the generic base class. Every class, derived from this class in a direct or indirect manner can be administrated by the factory. Therefore, the Object can be regarded as a lightweight base providing a set of functions to obtain the associated Class object.

Class object

The Class is the central object, which is responsible for creating instances of a class and provides class specific information in terms of limited class reflection. One Class object is always associated with a specific class definition. Therefore, every class, which is derived from Object, will have its own Class object. As a part of the provided reflection, each Class object knows its derived child classes in terms of the associated Class objects. Therefore, every Class object can be regarded as a class factory for its derived children. Consequently, the Factory class is nothing more than a thin wrapper around the root Object class, which knows every child class derived from Object.

Interplay of the three basic classes.

Basically there are two different ways for obtaining the Class object for a certain class, which is derived from Object. If the type of the class is known during compile time, you may want to use the static CLASS() function (e.g. ExampleClass::CLASS() ). Otherwise, you can use the dynamic equivalent by calling the getClass() function (e.g. ExampleClass::getClass()). If you do not know anything else than the identifier of your class, you may wish to use the Factory to obtain the associated Class object or to create instances of the class.

Quick start

This quick start will show you the basic idea of how you can make use of the class Factory.

1. Derive your class from the Object class

In order to support class creation using the factory, your class should be able to expose class specific information to the class factory. This can be done in a two step process. First of all you have to derive your class from the Object class. Doing so, some basic functions, returning a specialized Class object, will be added to your class. Furthermore, you have to make use of the MIRA_OBJECT() macro within your class declaration in order to add the correct identifier of your class to the specialized Class object. Please note that the factory will automatically add the full namespace to the class name. Therefore, the identifier of Object MyObject in namespace subspace will be subspace::MyObject. The MIRA_OBJECT() macro will also extract the "real" name to have a more handy version.

Following the instructions your class may look something like this:

class MyClass : virtual public Object
{
MIRA_OBJECT( MyClass )
public:
...
};

Now your class has all functions necessary to return the class specific Class object using a common basic interface. However, your class is not registered at the class factory at this point of time.

2. Register your class

After you have equipped your class with the Object interface in the previous step, you have to register your new class at the class Factory. Therefore, you have to use the MIRA_CLASS_REGISTER() macro (preferably within the implementation file of your class and outside of every namespace). With this macro you register the class at the Factory and tell the factory the parents of your classes. Therefore, if your class MyClass is derived from Object only, the macro should look like the following:

If your class is derived from two or more classes, you simply have to add the parents to the macro (separated by commas). For example if your class is (directly) derived from class A and class B the registration should look like:

MIRA_CLASS_REGISTER( MyClass, A, B );

Now, your object is known by the class factory whenever the library containing your class (or the manifest file containing your class) is loaded.

Please note: If you want your class to be part of the Serialization framework you have to use the MIRA_CLASS_SERIALIZATION macro. Therefore the previous code changes to:

MIRA_CLASS_SERIALIZATION( MyClass, A, B );

3. Getting access to the Class object of your class

By deriving your class from the base class Object, a small set of interface functions have been added to your class. Therefore, you can get access to the class specific Class object in three different ways:

Using the static CLASS() function:

This is the common practice to obtain the Class object, if the full type information (definition) of your class is available. If this is the case, you can access the class object by calling

ClassProxy tClassObjectOfMyClass = MyClass::CLASS();

Using the dynamic getClass() function:

This is the preferred way to obtain the Class object, if no type information but an instance of your class is available (e.g. if your class is processed by a library which does not have access to the class definition.). Therefore, accessing the class object may look like:

Object* tPointerToMyClass = (...);
ClassProxy tClassObjectOfMyClass = tPointerToMyClass->getClass();

Using the static class factory

You may wish to use this procedure, if you do have nothing else but the class identifier of your class (neither the type information of your class is available, nor an instance of your class is). Then you may wish to use the following code:

string tMyClassIdentifier = "mira::MyClass";
ClassProxy tClassObjectOfMyClass = ClassFactory::getClassByIdentifier( tMyClassIdentifier );

However, if the exact definition of your class is missing, while the definition of one or more base classes is still available, you can use one of the base classes to get access to the specific Class object of your class:

string tMyClassIdentifier = "mira::MyClass";
ClassProxy tClassObjectOfMyClass = BaseClass::CLASS().getClassByIdentifier( tMyClassIdentifier );

4. What to do with the Class object

Now, you should be able to obtain a Class object of your own class in several different ways. However, the natural question - what the Class object is all about - still remains. Although, the Class object can be useful in different cases, there are three main purposes:

Imagine a pointer tPointer of type MyClass pointing at an instance of MyClass. You can obtain information about the class using the getClass() function. E.g. if you want to obtain the name of your class, you have to call:

std::string tName = tPointer->getClass().getName();

Of course, you can create new instances of the class (which you may not know exactly). This can be done by calling:

MyClass* tPointer2 = tPointer->getClass().newInstance();

Note that the returned class is of the same type as the pointer you are using to call the getClass() function. As mentioned before, there are several different ways to get access to the Class object. Therefore, the following code may be of interest, if you don't already have an instance of your class:

MyClass* tPointer3 = ClassFactory::getClassByIdentifier("mira::MyClass").newInstance();

However, for convenience, there is an even shorter version, to do this:

MyClass* tPointer4 = ClassFactory::newInstance<MyClass>("mira::MyClass");

where the template parameter can be used to cast the created object to any type in the class hierarchy of MyClass.

As mentioned before, our factory concept is not limited to construction of single objects. Therefore, you can use the class factory to obtain information of your class. The following lines will give you some examples. Assume that you have access to the class object of MyObjecty by tClass:

string tClassIdentifier = tClass.getIdentifier();
// -> tClassIdentifier = "myNamespace::MyClass"
string tClassName = tClass.getName();
// -> tClassName = "MyClass"

note that the name of the class did not contain the class namespace and therefore may not be unique.

Beside the tClass object the following code assumes tBaseClass to be a Class object of an abstract base class of MyClass.

tBool = tClass.isDerivedFrom( tBaseClass );
// -> tBool = true
tBool = tClass.isBaseOf( tBaseClass );
// -> tBool = false
tBool = tClass.isAbstract();
// -> tBool = false (if MyClass is not abstract)
tBool = tBaseClass.isAbstract();
// -> tBool = true

Furthermore, you can get a list of derived children of a class. This may be helpful if you want to know which classes are available during runtime. E.g. if you want to implement some kind of plugin concept (where the plugins are derived from a common base class and are implemented in external libraries):

std::map<std::string, ClassProxy > tDerivedChildren;
tDerivedChildren = tBaseClass.getDerivedClasses();
// print a list of children: Identifier of class - name of class
foreach( tChild, tDerivedChildren) {
std::cout << tChild.first << " - " << tChild.getName() << "\n";
}

Since you are familiar with the basic concepts of the class, you can proceed with the advanced usage now.

Advanced features

Add meta information to your class

Meta information can be really helpful if you want to enrich your class with some attributes which are not naturally expressed by the class hierarchy. E.g. if you have implemented a new base class MyBall. All necessary functions and attributes are ready to use and your ball is looking good. However, later on you realize that some specialized class implementations of your MyBall base class behave strange and unpredictable when played by a bad player. For some reason, you want the class factory to provide a list of the strange behaving balls only (think of an April fool). Of course, you could add a property to your base class which expresses if a ball behaves strange or not. However, this doesn't help since you have to create an instance of every ball and check the property manually. The second idea is to derive all strange behaving balls from a new base class. Again, this is quite inconvenient and may lead to a very unnatural class hierarchy. Nevertheless, the class factory supports to add meta information to all classes. In contrast to other members of the class, this information can be accessed before an instance of the class is available. E.g. you can add the property "strange behavior" as meta information as follows:

class MyStrangeBall : public MyBall
{
MIRA_META_OBJECT(MyStrangeBall,("behavior","strange"))
...
};

Now, you can get a list of strange behaving balls:

std::vector<ClassProxy> tStrangeBalls;
tStrangeBalls = MyBall::CLASS().getClassByMeta("behavior","strange");
foreach( ClassProxy tClass, tStrangeBalls ) {
std::cout << tClass.getIdentifier() << "\n";
}

Please note: Meta information is inherited from base classes to the children. Therefore, a child class of the strange behaving ball is always behaving strange as long as the meta key is not overwritten in the child class.

Mark abstract classes

Registering classes at the class factory allows to instantiate them not only through writing and compiling code, but also at runtime by referring to them by name or even by ancestry (e.g. iterate over all subclasses of X and instantiate them).

Normally, when a class is abstract (because it has pure virtual methods, as it is defining an interface meant to be implemented by subclasses), the compiler will exit with an error message when trying to instantiate an object of that class. With the class factory, there may not be specific instantiation code compiled where an error could occur. Nevertheless, the class factory registration and manifest generation detects the abstractness of a class and notices that this class cannot be instantiated. The sole existence and registration at the factory of an abstract class is not an error, it may not be meant to be ever instantiated, but just e.g. as a common base class to enable identifying all its subclasses.

However, this mechanism can also make it hard to spot when a class is abstract inadvertently, which may not even be a mistake by that classes designer, e.g. when subclassing a base class (and fully implementing it in case of an abstract base) and the base is later extended with (additional) pure virtual methods. Without any means to know if it is abstract on purpose, the only observable effect may be that the class factory will never instantiate it, in many cases (e.g. when finding by ancestry) just silently ignoring that class. So this would lead to more or less subtle errors at runtime, which may even remain unnoticed until a user tries to use a certain tool or feature. To prevent this and allow detection of inadvertently abstract classes at compile time, abstract classes must be explicitly marked and the manifest generation will verify that the declaration and the actual abstractness match for each registered class.

To this purpose, MIRA_ABSTRACT_OBJECT() is used instead of MIRA_OBJECT()

class MyInterface : public Object
{
...
};
class MyStrangeBallInterface : public MyBall
{
MIRA_ABSTRACT_META_OBJECT(MyStrangeBallInterface,("behavior","strange")))
...
};

Use other constructors than the default one

In some cases, a simple call of newInstance() may be insufficient to construct the desired object. E.g. if the class to construct did not have a default constructor:

class MyConstructorClass : public Object
{
MIRA_OBJECT(MyConstructorClass)
public:
MyConstructorClass(int intOne, char charOne) : mInt(intOne), mChar(charOne) {
}
public:
int mInt;
char mChar;
};

As you can see, you don't have to change anything within the class declaration. However, the registration at the factory have to be extended:

MIRA_OBJECT_CONSTRUCTOR2(MyConstructorClass, int,char );
MIRA_CLASS_REGISTER( MyConstructorClass, mira::Object );

You have to use the MIRA_OBJECT_CONSTRUCTOR2 Macro in addition to the MIRA_CLASS_REGISTER macro to tell the factory that a constructor with two arguments is available for class MyConstructorClass class. The following arguments of the macro tells the factory of what types the constructor expects for the arguments. In consequence, you have to use the MIRA_CLASS_REGISTER3 macro if you like to announce a constructor with three arguments... The construction of class instances using one of the newInstance() functions is straight forward. E.g. it may look like the following:

MyConstructorClass* tInstance = MyConstructorClass::CLASS().newInstance(2,30000,4);

The only thing you have to care about is the first argument, which announces the number of following arguments.

The manifest files

Sometimes it may be not appropriate to load all libraries just to know every class which can be constructed with the class factory. Therefore, the class factory supports the import and export of manifest files. The manifest files can be regarded as a description of the classes contained within a certain library. Therefore, loading the manifest files of a set of libraries is equivalent to loading the corresponding libraries for the "user" of the factory. As soon as an instance of a class is requested the factory automatically loads the corresponding library.

Since the MIRA build system (see Build System) already supports the construction of manifest files, you normally don't have to care about this part of the process. Once created you can load manifest files using the ManifestAgent. (Please note that you even don't have to care about this step if you are using mira or miracenter). Therefore, your code may look like:

ManifestAgent tAgent;
tAgent.loadFile("MyLibrary.manifest");
tAgent.loadFile("MyLibrary2.manifest");
tAgent.finalize();


The trailing finalize step is necessary to trigger the class factory to build the correct dependency graph of the involved classes. Now you can use the class factory and your classes according to the usual practice.

FAQ

Question: I got one of the following error messages, but everything looks fine.

Answer: Congratulations! Probably you derived (directly or indirectly) from one class twice and didn't overwrite the ambiguous functions a third time (Diamond of death). This happens if you derive from two classes which are in turn derived from Object. Solution: Use the virtual keyword to solve the ambiguity. class XX : virtual public Object {

Question: I got the following error message: "cannot access Yourclass::Yourclass()..."

Answer: Your compiler is not capable of automatically detecting if a class does have a public default constructor. Don't panic, you only have to put the MIRA_NO_PUBLIC_DEFAULT_CONSTRUCTOR macro directly after your Object macro and everything will be fine.