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 templated CatalogInterface name.
  • Base defines a GetCatalog() function that returns a static instantiation of a CatalogInterface::CatalogType. The CatalogInterface::GetCatalog() function actually calls this function within the base class. This means that the base class actually owns the catalog, and the CatalogInterface is only operating on that Base::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 registered Base’s catalog.
  • The new type must be registered with the catalog held in Base. To accomplish this, a convenience macro REGISTER_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.