OpenFOAM guide/runTimeSelection mechanism
OpenFOAM's runtime selection mechanism is a templated implementation of an idiom in C++ known as a "virtual constructor" or "Factory Method of initialization". Most of its implementation takes place within macros, so it is hidden from the programmer.
Contents
1 Why is it needed?
In fully object-oriented programming, an object will often interface with a generic base class to manipulate derived classes. This greatly simplifies top-level code, and makes adding future derived classes easy. Functions called through their base classes are known as virtual functions. C++ allows for these types of functions, with the exception of constructors. Therefore, if an OpenFOAM object wants to construct a derived class using a base class as its interface, it cannot.
For instance, a solver interfaces with a generic boundary condition class (fvPatchField for finite volume). The solver will start by creating the fields (createFields.H), but it does not have access to the derived boundary condition classes. So there are two options:
- make the solver interface with the individual boundary conditions; or
- implement a "virtual constructor" work-around.
There are dozens of boundary conditions and dozens of solvers, so the preference would be the virtual constructor.
2 When do you need it?
Anytime you have an object that interfaces with a base class, and you need to construct a derived class, you will need it. Either you are working with a set of OpenFOAM classes that already have runtime selection implemented (such as writing a custom boundary condition), or you are creating an entirely new set of classes and need to use runtime selection.
3 How do you use it?
It is easiest to find a derived class that most closely matches what you intend, copy it, rename it, search and replace the old name with the new name in all the files, then modify the code as necessary. This way you reduce the risk of "breaking" the runtime selection. However, it helps to understand what the components of runtime selection are.
3.1 Runtime selection components
Say we have a base class, Base, and a set of derived classes Derived, including Derived1, Derived2 and Derived3. Also, say you want two constructors to act as virtual constructors:
Base(dictionary& dict); Base(label& place, scalar& magnitude, Istream& is);
To keep track, we give them informal names. Call the first one MrConstructor and the second, MrsConstructor.
Runtime selection works by creating a hash table of Derived constructor pointers in Base. To accomplish this, Base requires hash table creator and destroyer functions as well as a subclass AddToTable that Derived uses to register its constructor. (The details of these functions are in the Detailed implementation section.)
The main components of runtime selection are:
- declareRunTimeSelectionTable - macro invoked in Base.H
- defineRunTimeSelectionTable - macro invoked in Base.C
- addToRunTimeSelectionTable - macro invoked in Derived.C
- New - "Selector" function added to Base.
3.1.1 declareRunTimeSelectionTable
Think of this as the header for the runtime selection changes to Base.
3.1.1.1 What it does
This macro adds to Base.H:
- typedefs for the constructor pointer and hash table data types
- the hash table pointer
- prototypes for the hash table creator and destroyer functions
- the full definition of the AddToTable subclass
3.1.1.2 How to use it
For every constructor type (e.g. MrConstructor, MrsConstructor, etc..) there should be a declareRunTimeSelectionTable entry in Base.H:
declareRunTimeSelectionTable(autoPtr,baseType,argNames,argList,parList);
- autoPtr - either tmp (for large objects) or autoPtr. This is the type of wrapper class to be used to return the constructed Derived.
- baseType - this is Base.
- argNames - this is a small name you gave to the parameter list (i.e. MrConstructor and MrsConstructor above).
- argList - this is the full parameter list with modifiers, types and names - enclose in ()'s.
- parList - this is the parameter list with names only.
Example: fvPatchField.H
declareRunTimeSelectionTable ( tmp, fvPatchField, patch, ( const fvPatch& p, const DimensionedField<Type, volMesh>& iF ), (p, iF) );
3.1.2 defineRunTimeSelectionTable
There are some variations to this macro depending on the extent of templating. Think of this as the body for runtime selection changes to Base.C.
3.1.2.1 What it does
This macro adds to Base.C:
- initialization for the hash table pointer (necessary as it is static)
- the body for the the hash table creator and destroyer functions
3.1.2.2 How to use it
For every constructor type (e.g. MrConstructor, MrsConstructor, etc..) there should be a defineRunTimeSelectionTable entry in Base.C:
defineRunTimeSelectionTable(baseType,argNames)
The arguments are the same as above:
- baseType - this is Base.
- argNames - this is a small name you gave to the parameter list (i.e. MrConstructor and MrsConstructor above).
Example: RASModel.C
defineRunTimeSelectionTable(RASModel, dictionary);
3.1.3 addToRunTimeSelectionTable
This is a tiny snippet of code that adds a Derived constructor to the hash table held by Base. There are a variety of implementations of this macro depending on the templating.
3.1.3.1 What it does
Adds an AddToTable member variable to Base, which in turn adds Derived's constructor to the hash table.
3.1.3.2 How to use it
For every constructor type, an addToRunTimeSelectionTable call should be made in Derived.C:
addToRunTimeSelectionTable(baseType,thisType,argNames)
The arguments are:
- baseType - this is Base.
- thisType - this is Derived1, Derived2 or Derived3.
- argNames - this is a small name you gave to the parameter list (i.e. MrConstructor and MrsConstructor above).
3.1.4 Selectors
Selectors act as the virtual constructor function itself. They are declared in Base, and in OpenFOAM they are named New.
3.1.4.1 What they do
The selector looks through the function parameters to determine the typeName of the Derived to be constructed. It uses the typeName to look up the constructor in the hash table, and returns the constructor pointer it finds.
3.1.4.2 How to use them
There are no macros available for this. You need to fully implement the selector functions yourself. For every constructor type, you need to declare a New in Base.H and implement it in Base.C. Follow examples of other selectors already written.
3.1.5 Complications
In some situations, the runtime selection mechanism is further complicated with:
- additional templating; and
- class hierarchy.
3.1.5.1 Templating
Some of the macros may be hidden within other macros depending on the level of templating. For instance, if Base is itself a templated function Base<Type>, then the derived functions will have to:
- set their own typeName for all expected Type's; and
- register all expected Type's to the constructor table.
This can be accomplished with a typedef macro in Derived.H and a macro to amass all the addToRunTimeSelectionTable entries in Derived.C.
Another common occurance involves the typeName macros. All the classes affected by runtime selection must have a typeName, accomplished with the TypeName macro in the header, and one of the defineTypeNameAndDebug macros in the body. To streamline the process, the addToRunTimeSelectionTable and defineTypeNameAndDebug macros can (and often are) called together in another macro.
Both of the situations above mean the addToRunTimeSelectionTable will not be directly visible in Derived.C.
3.1.5.2 Class hierarchy
Some situations arise where two runtime selection groups overlap in the class hierarchy. This can lead to a case where a Derived class of one group is also the Base of another group. In this case, a class will have both:
- macros belonging to a Base; and
- macros belonging to a Derived.
Just focus on the macros you expect to see. The other macros will not interfere with it.
4 How does it work?
In this section, a closer look is taken at what OpenFOAM's runtime selection is doing. A pared-down version of runtime selection is implemented in detail.
4.1 Virtual functions
Any function defined with the virtual key word in a base class and redefined in its derived class is a virtual function. Without the virtual key word, the function is hidden. An object can use a base class pointer to manipulate a derived class object. This is accomplished behind the scenes through the use of pointers and hash tables. A hash table (called a vtable) is created for each class that has at least one virtual function, and each class has a pointer (called a vpointer) to its vtable. The entries in the vtable are pointers to all the virtual functions owned by that class. When a virtual function is called, the correct function is found by following the vpointer and looking it up on the vtable.
Unfortunately, a vtable cannot be constructed until the object in question is fully manifested. In other words, no virtual constructors.
4.2 Solution Overview
The solution is to create your own vtable's and vpointer's:
- Create a hash table in Base that contains constructor pointers indexed by typeName.
- Have all the Derived add themselves to the list:
- Create a subclass AddToTable within Base whose constructor adds an entry to the hash table
- Add a line in Derived.C that creates an instance of Base::AddToTable
- Create a "selector" function in Base called New that:
- is called like a regular Base constructor function;
- first looks through the parameters to determine the typeName of the Derived that should be constructed;
- uses the typeName to look up the correct Derived constructor pointer in the hash table; and
- returns the constructor pointer.
- Wrap all these changes into macros so the programmer doesn't see them, and only has to worry about calling New instead of new.
4.3 Detailed implementation
This section shows the changes to the code that need to be made.
4.3.1 General changes
To implement a runtime selection mechanism:
- Decide on one or more sets of parameter lists for the virtual constructors.
- If there is more than one, give them each a short, informal name. For example:
- Base or Derived( dictionary& ) is MrConstructor, and
- Base or Derived( label&, scalar&, Istream& ) is MrsConstructor.
- Data within the parameters must reveal the typeName of the intended Derived type.
- If there is more than one, give them each a short, informal name. For example:
- Implement these as regular constructors in Base and Derived.
4.3.2 Adding a vtable and vpointer to Base
Most of the changes happen to Base. In Base you need to:
- create a data type that is a pointer to each constructor type:
In Base.H:
typedef Base (*MrConstructorPtr)( const dictionary& dict) typedef Base (*MrsConstructorPtr) ( const label& place, const scalar& magnitude, const Istream& dataFlow );
- create a static pointer to a hash table of these function pointers:
In Base.H:
// Showing only MrConstructor for now on typedef HashTable<MrConstructorPtr, word, string::hash> MrConstructorTable; static MrConstructorTable* MrConstructorTablePtr_;
In Base.C:
//This line is needed because it is static Base::MrConstructorTable* Base::MrConstructorTablePtr_ = NULL;
- Lastly, add static functions that create and delete the hash table:
In Base.H:
static void constructMrConstructorTables(); static void destroyMrConstructorTables();
In Base.C:
void Base::constructMrConstructorTables() { static bool constructed = false; // this is necessary because it will be called many times if (!constructed) { Base::MrConstructorTablePtr_ = new Base::MrConstructorTable; constructed = true; } } void Base::destroyMrConstructorTables() { if (Base::MrConstructorTablePtr_) { delete Base::MrConstructorTablePtr_; Base::MrConstructorTablePtr_ = NULL; } }
4.3.3 Getting derived classes to register
To force Derived to register to the constructor table, OpenFOAM uses a clever trick:
- First Base is given an extra subclass whose constructor inserts new entries into the hash table, templated on the Derived type:
In Base.H:
template<class DerivedType> class addMrConstructorToTable { public: static Base New ( const dictionary& dict) { return Base(new DerivedType (dict)); } addMrConstructorToTable ( const word& lookup = DerivedType::typeName ) { constructMrConstructorTables(); MrConstructorTablePtr_->insert(lookup, New); } ~addMrConstructorToTable() { destroyMrConstructorTables(); } };
- Lastly, all Derived classes have one extra line in their Derived.C:
Base::addMrConstructorToTable<DerivedType>
addDerivedTypeMrConstructorToBaseTable_;
Although this is in Derived.C, it is adding an instance of Base::AddToTable to the Base class. When Base is fully read, there will be a complete vtable with all the Derived constructors.
4.3.4 Selector - the virtual constructor function
The last step is to create a function in Base that takes constructor calls, looks up the correct constructor in the table, and returns a pointer to it. They are declared in Base, and in OpenFOAM they are named New. A simple Selector might look like this: In Base.H:
//- Return a pointer to a new Derived created on freestore // from dictionary static tmp<Base> New(const dictionary&);
In Base.C (or BaseNew.C):
Foam::tmp<Foam::Base> Foam::Base::New(const dictionary& dict) { // omitting error catching and debug statements word DerivedType(dict.lookup("type")); typename MrConstructorTable::iterator cstrIter = MrConstructorTablePtr_->find(DerivedType); return cstrIter()(dict); }
4.3.5 When does this all happen?
The "virtual constructor" table is fully built and loaded into memory when the include list is fully read, and before the first line of code is executed. This is achieved using the static keyword. All static members of a class exist prior to any instance of that class. Marupio 16:35, 26 February 2010 (UTC)