ObjectCatalog¶
The “ObjectCatalog” is a collection of classes that acts as a statically initialized factory.
It functions in a similar manner to a classic
factory method,
except that there is no maintained list of derived objects that is required to create new objects.
In other words, there is no case-switch/if-elseif block to maintain.
Instead, the ObjectCatalog
creates a “catalog” of derived objects using a std::unordered_map
.
The “catalog” is filled when new types are declared through the declaration of a helper class named
CatalogEntryConstructor
.
The key functional features of the “ObjectCatalog” concept may be summarized as:
- Anonymous addition of new objects to the catalog. Because we use a statically initialized singleton map object to store the catalog, no knowledge of the contents of the catalog is required in the main code. Therefore if a proprietary/sensitive catalog entry is desired, it is only required that the object definition be outside of the main repository and tied into the build system through some non-specific mechanism (i.e. a link in the src/externalComponents directory) and the catalog entry will be registered in the catalog without sharing any knowledge of its existence. Then a proprietary input file may refer to the object to call for its creation.
- Zero maintenance catalog. Again, because we use a singleton map to store the catalog, there is no updating of code required to add new entries into the catalog. The only modifications required are the actual source files of the catalog entry, as described in the Usage section below.
Implementation Details¶
There are three key objects that are used to provide the ObjectCatalog functionality.
CatalogInterface¶
The CatalogInterface
class provides the base definitions and interface for the
ObjectCatalog concept.
It is templated on the common base class of all derived objects that are
creatable by the “ObjectCatalog”.
In addition, CatalogInterface
is templated on a variadic parameter pack that
allows for an arbitrary constructor argument list as shown in the declaration shown below:
template< typename BASETYPE, typename ... ARGS >
class CatalogInterface
The CatalogInterface
also defines the actual catalog type using the template arguments:
typedef std::unordered_map< std::string,
std::unique_ptr< CatalogInterface< BASETYPE, ARGS... > > > CatalogType;
The CatalogInterface::CatalogType
is a std::unordered_map
with a string “key” and a value
type that is a pointer to the CatalogInterface that represents a specific combination of
BASETYPE
and constructor arguments.
After from setting up and populating the catalog, which will be described in the “Usage” section,
the only interface with the catalog will typically be when the Factory()
method is called.
The definition of the method is given as:
static std::unique_ptr< BASETYPE > Factory( std::string const & objectTypeName, ARGS... args )
{
return GetCatalog().at( objectTypeName ).get()->Allocate( args ... );
}
It can be seen that the static Factory
method is simply a wrapper that calls the virtual
Allocate
method on a the catalog which is returned by GetCatalog()
.
The usage of the Factory
method will be further discussed in the Usage section.
Note
The method for organizing constructing new objects relies on a common constructor list between
the derived type and the BASETYPE
.
This means that there is a single catalog for each combination of BASETYPE
and the variadic
parameter pack representing the constructor arguments.
In the future, we can investigate removing this restriction and allowing for construction of
a hierarchy of objects with an arbitrary constructor parameter list.
CatalogEntry¶
The CatalogEntry
class derives from CatalogInterface
and adds the a TYPE
template argument
to the arguments of the CatalogInterface
.
template< typename BASETYPE, typename TYPE, typename ... ARGS >
class CatalogEntry final : public CatalogInterface< BASETYPE, ARGS... >
The TYPE
template argument is the type of the object that you would like to be able to create
with the “ObjectCatalog”.
TYPE
must be derived from BASETYPE
and have a constructor that matches the variadic parameter
pack specified in the template parameter list.
The main purpose of the CatalogEntry
is to override the CatalogInterface::Allocate()
virtual
function s.t. when key is retrieved from the catalog, then it is possible to create a new TYPE
.
The CatalogEntry::Allocate()
function is a simple creation of the underlying TYPE
as shown by
its definition:
virtual std::unique_ptr< BASETYPE > Allocate( ARGS... args ) const override
{
#if OBJECTCATALOGVERBOSE > 0
GEOSX_LOG( "Creating type " << LvArray::system::demangle( typeid(TYPE).name())
<< " from catalog of " << LvArray::system::demangle( typeid(BASETYPE).name()));
#endif
#if ( __cplusplus >= 201402L )
return std::make_unique< TYPE >( args ... );
#else
return std::unique_ptr< BASETYPE >( new TYPE( args ... ) );
#endif
}
CatalogEntryConstructor¶
The CatalogEntryConstructor
is a helper class that has a sole purpose of creating a
new CatalogEntry
and adding it to the catalog.
When a new CatalogEntryConstructor
is created, a new CatalogEntry
entry is created and
inserted into the catalog automatically.
Usage¶
Creating A New Catalog¶
When creating a new “ObjectCatalog”, it typically is done within the context of a specific
BASETYPE
.
A simple example of a class hierarchy in which we would like to use the “ObjectCatalog”
to use to generate new objects is given in the unit test located in testObjectCatalog.cpp
.
The base class for this example is defined as:
class Base
{
public:
Base( int & junk, double const & junk2 )
{
GEOSX_LOG( "calling Base constructor with arguments ("<<junk<<" "<<junk2<<")" );
}
virtual ~Base()
{
GEOSX_LOG( "calling Base destructor" );
}
using CatalogInterface = dataRepository::CatalogInterface< Base, int &, double const & >;
static CatalogInterface::CatalogType & GetCatalog()
{
static CatalogInterface::CatalogType catalog;
return catalog;
}
virtual std::string getCatalogName() = 0;
};
There a couple of things to note in the definition of Base
:
Base
has a convenience alias to use in place of the fully templatedCatalogInterface
name.Base
defines aGetCatalog()
function that returns a static instantiation of aCatalogInterface::CatalogType
. TheCatalogInterface::GetCatalog()
function actually calls this function within the base class. This means that the base class actually owns the catalog, and theCatalogInterface
is only operating on thatBase::GetCatalog()
, and that the definition of this function is required.
Adding A New Type To The Catalog¶
Once a Base
class is defined with the required features, the next step is to add a new derived
type to the catalog defined in Base
.
There are three requirements for the new type to be registered in the catalog:
- The derived type must have a constructor with the arguments specified by the variadic parameter pack specified in the catalog.
- There must be a static function
static string CatalogName()
that returns the name of the type that will be used to as keyname when it is registeredBase
’s catalog. - The new type must be registered with the catalog held in
Base
. To accomplish this, a convenience macroREGISTER_CATALOG_ENTRY()
is provided. The arguments to this macro are the name type of Base, the type of the derived class, and then the variadic pack of constructor arguments.
A pair of of simple derived class that have the required methods are used in the unit test.
class Derived1 : public Base
{
public:
Derived1( int & junk, double const & junk2 ):
Base( junk, junk2 )
{
GEOSX_LOG( "calling Derived1 constructor with arguments ("<<junk<<" "<<junk2<<")" );
}
~Derived1()
{
GEOSX_LOG( "calling Derived1 destructor" );
}
static std::string CatalogName() { return "derived1"; }
std::string getCatalogName() { return CatalogName(); }
};
REGISTER_CATALOG_ENTRY( Base, Derived1, int &, double const & )
class Derived2 : public Base
{
public:
Derived2( int & junk, double const & junk2 ):
Base( junk, junk2 )
{
GEOSX_LOG( "calling Derived2 constructor with arguments ("<<junk<<" "<<junk2<<")" );
}
~Derived2()
{
GEOSX_LOG( "calling Derived2 destructor" );
}
static std::string CatalogName() { return "derived2"; }
std::string getCatalogName() { return CatalogName(); }
};
REGISTER_CATALOG_ENTRY( Base, Derived2, int &, double const & )
Allocating A New Object From The Catalog¶
The test function in the unit test shows how to allocate a new object of one
of the derived types from Factory
method.
Note the call to Factory
is scoped by Base::CatalogInterface
, which is
an alias to the full templated instantiation of CatalogInterface
.
The arguments for Factory
TEST( testObjectCatalog, testRegistration )
{
GEOSX_LOG( "EXECUTING MAIN" );
int junk = 1;
double junk2 = 3.14;
// allocate a new Derived1 object
std::unique_ptr< Base >
derived1 = Base::CatalogInterface::Factory( "derived1", junk, junk2 );
// allocate a new Derived2 object
std::unique_ptr< Base >
derived2 = Base::CatalogInterface::Factory( "derived2", junk, junk2 );
EXPECT_STREQ( derived1->getCatalogName().c_str(),
Derived1::CatalogName().c_str() );
EXPECT_STREQ( derived2->getCatalogName().c_str(),
Derived2::CatalogName().c_str() );
GEOSX_LOG( "EXITING MAIN" );
}
The unit test creates two new objects of type Derived1
and Derived2
using the
catalogs Factory
method.
Then the test checks to see that the objects that were created are of the correct type.
This unit test has some extra output to screen to help with understanding of the
sequence of events.
The result of running this test is:
$ tests/testObjectCatalog
Calling constructor for CatalogEntryConstructor< Derived1 , Base , ... >
Calling constructor for CatalogInterface< Base , ... >
Calling constructor for CatalogEntry< Derived1 , Base , ... >
Registered Base catalog component of derived type Derived1 where Derived1::CatalogName() = derived1
Calling constructor for CatalogEntryConstructor< Derived2 , Base , ... >
Calling constructor for CatalogInterface< Base , ... >
Calling constructor for CatalogEntry< Derived2 , Base , ... >
Registered Base catalog component of derived type Derived2 where Derived2::CatalogName() = derived2
Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from testObjectCatalog
[ RUN ] testObjectCatalog.testRegistration
EXECUTING MAIN
Creating type Derived1 from catalog of Base
calling Base constructor with arguments (1 3.14)
calling Derived1 constructor with arguments (1 3.14)
Creating type Derived2 from catalog of Base
calling Base constructor with arguments (1 3.14)
calling Derived2 constructor with arguments (1 3.14)
EXITING MAIN
calling Derived2 destructor
calling Base destructor
calling Derived1 destructor
calling Base destructor
[ OK ] testObjectCatalog.testRegistration (0 ms)
[----------] 1 test from testObjectCatalog (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
Calling destructor for CatalogEntryConstructor< Derived2 , Base , ... >
Calling destructor for CatalogEntryConstructor< Derived1 , Base , ... >
Calling destructor for CatalogEntry< Derived2 , Base , ... >
Calling destructor for CatalogInterface< Base , ... >
Calling destructor for CatalogEntry< Derived1 , Base , ... >
Calling destructor for CatalogInterface< Base , ... >
In the preceding output, it is clear that the static catalog in Base::GetCatalog()
is initialized prior the execution of main, and destroyed after the completion of main.
In practice, there have been no indicators of problems due to the use of a statically
initialized/deinitialized catalog.