Extending `TypeContainer` for run-time objects

Proposal: Add a RuntimeTypeContainer

In order to support run-time arc and node types for graph resources, I’m proposing to create a new RuntimeTypeContainer that inherits TypeContainer: it would hold objects by

  • their exact compile-time type-name when inserted at compile time and
  • a “declared” (user-provided) type-name when inserted at run time.

Furthermore, when adding an object using the run-time API, it would record the common base type and the “declared” type of the inserted/emplaced object. This allows for inspection of the container at run time by code that can handle objects of that particular base type. (See the runtimeBaseTypes() and runtimeTypeNames() methods in the example that follows.)

There is a merge request up that implements this proposal.

Insertion and retrieval

For example, consider a TypeContainer used to hold metadata objects. Compile-time objects can be inserted during the build but users may need to add “programmable” metadata objects at run-time. The application can provide a base RuntimeMetadata class with virtual methods and then allow users to “name” instances of this base class as if each represented its own type.

struct MetadataA { const char* name = "Foo"; };
struct MetadataB { const char* name = "Bar"; };

class RuntimeMetadata
{
public:
  RuntimeMetadata(const std::string& name) : m_name(name) { }
  virtual std::string name() const { return m_name; }
private:
  std::string m_name;
};

class Baz : public RuntimeMetadata
{
public:
  Baz() : RuntimeMetadata("Baz") { };
};

RuntimeTypeContainer allMetadata(MetadataA(), MetadataB());
// … some time later:
allMetadata.insertRuntime<RuntimeMetadata>("Baz", Baz());
allMetadata.emplaceRuntime<RuntimeMetadata>("Bay", "Arf");

In this example, there are now two “compile-time” entries in allMetadata plus two “run-time” entries. You can fetch “Foo” and “Bar” by knowing their type at compile time:

auto& foo = allMetadata.get<Foo>();
auto& bar = allMetadata.get<Bar>();

You can fetch the others by knowing either their exact type (if their user-provided type-name matches their actual type-name) or that they inherit RuntimeMetadata:

// We can use get<Baz>() because the declared type-name
// matches the actual type-name:
auto bazDirect = allMetadata.get<Baz>();
// Or we can fetch using the run-time API:
auto bazByBase = allMetadata.getRuntime<RuntimeMetadata>("Baz");

// We can't use get<Bay>() because there is no such type
// nor get<RuntimeMetadata>() because "Bay" was provided
// as the "type-name." But we can use the run-time API:
auto bay = allMetadata.getRuntime<RuntimeMetadata>("Bay");

std::cout << bazByBase.name() << " and " << bay.name() << "\n";
// Prints "Baz and Arf".

Importantly, (1) you can store multiple “run-time” objects sharing the same base class in the type container as long as their declared types are unique and (2) the declared type does not need to match any actual type. However, (3) if fetching by a run-time object’s base type, the template parameter must exactly match the type used to insert the object into the TypeContainer.

For example:

class Xyzzy : public Baz { };
allMetadata.insertRuntime<RuntimeMetadata>("Xyzzy", Xyzzy());

// Will NOT work:
auto dataBad = allMetadata.getRuntime<Baz>("Xyzzy");

// Correct, because insertRuntime was called with
// the same template parameter:
auto dataGood = allMetadata.getRuntime<RuntimeMetadata>("Xyzzy");

Introspection of runtime type-data

It is also important for consumers of a RuntimeTypeContainer to be able to fetch the list of run-time objects and their base types. To this end, you can access a map from run-time storage types to the user-provided “type names.”

// Fetch a std::set<smtk::string::Token> naming
// the base types of runtime objects:
auto runtimeBases = allMetadata.runtimeBaseTypes();
auto runtimeBase = runtimeBases.front(); // "RuntimeMetadata"

// Return the "type-names" of objects that "inherit"
//  a given runtime base type:
auto runtimeObjectTypes = allMetadata.runtimeTypeNames(runtimeBase);

if (runtimeBase == "RuntimeMetadata"_token)
{
  std::cout << "RuntimeMetadata objects:\n";
  for (const auto& objectTypeName : runtimeObjectTypes)
  {
    auto obj =
      allMetadata.getRuntime<RuntimeMetadata>(objectTypeName);
    std::cout << "  " << obj.name() << "\n";
  }
}

// Prints:
// RuntimeMetadata objects:
//  Arf
//  Xyzzy
//  Baz

As you can see, the “base” type must be handled explicitly in order to fetch objects of a known type.

Intended usage

This new container will support run-time graph-resource nodes and arcs by allowing objects of potentially the same type to be inserted using different “declared” type-names. Thus, we can declare a single new arc type and create as many different instances of the type as needed; each one can then be inserted into the ArcMap type-container to represent a different arc type.