Sunday, October 30, 2016

Automatic collection of instances of derived classes in C++

Currently I work on application which measures and registers time that I'm spending in particular application. Since I use various of operating systems (Windows at work, and Linux at home), application has to be available on multiple platforms.
Sometimes I don't want to be tracked (e.g. when the screen is locked), so I've defined collection of rules, when tracking should be suspended:
std::vector<std::shared_ptr<Rule>> rules;
rules.push_back(std::make_shared<Rule1>());
rules.push_back(std::make_shared<Rule2>());
rules.push_back(std::make_shared<Rule3>());
rules.push_back(std::make_shared<Rule4>());
However, some of the rules are OS-specific (e.g. Rule2 and Rule3 compile and work only in Windows, and Rule4 makes sense only when app is running on Linux). So what I did:
  • Conditionally enabled files rule2.cc, rule3.cc and rule4.cc in the build system
    if (WIN32)
      set (RULE_FILES rule2.cc rule3.cc)
    elseif (UNIX)
      set (RULE_FILES rule4.cc)
    endif (WIN32)
    
  • Conditionally pushed instances of Rule2, Rule3 and Rule4 to the std::vector.
    std::vector<std::shared_ptr<Rule>> rules;
    rules.push_back(std::make_shared<Rule1>());
    #ifdef _WIN32 // Windows specific rules
      rules.push_back(std::make_shared<Rule2>());
      rules.push_back(std::make_shared<Rule3>());
    #elif __linux__ // Linux specific rules
      rules.push_back(std::make_shared<Rule4>());
    #endif
    
Two steps - sounds redundant. Also, I don't really like using ifdefs in the code (code with ifdefs is more difficult to maintain, and it's less readable). So I've implemented very simple class(actually, two classes), which helps creating a collection of instances of inherited classes.
Here's a code:
#include <memory>
#include <typeindex>
#include <unordered_map>

template <typename Derived, typename Base>
struct Registrar
{
  template<typename ...Args>
  Registrar(Args&&... args)
  {
    Base::registry()[typeid(Derived)] =
      std::make_shared<Derived>(std::forward<Args>(args)...);
  }
};

template<typename T>
struct Registrable
{
  typedef std::unordered_map<std::type_index, std::shared_ptr<T>> collection_t;
  template<typename R>
  using Registrar = Registrar<R, T>;

  static collection_t & registry()
  {
    static collection_t collection;
    return collection;
  }
};

I use std::unordered_map, because I want to make sure that I have only one instance of each class in my collection. If you accept, or even want to have multiple instances of the same class in the collection, you can replace it with std::vector, or any other collection.
And that's how the code above can be used:

class Base : public Registrable<Base>
{
public:
  virtual void print_myself() = 0;
};

class WithParameter : public Base
{
  int x;

public:
  WithParameter(int x) : x(x) {}

  void print_myself() override
  {
    std::cout << "WithParameter(" << x << ")" << std::endl;
  }

  static Registrar<WithParameter> registrar;
};
WithParameter::Registrar<WithParameter> WithParameter::registrar(25);

class NoParameter : public Base
{
public:
  void print_myself() override
  {
    std::cout << "NoParameter" << std::endl;
  }

  static Registrar<NoParameter> registrar;
};
NoParameter::Registrar<NoParameter> NoParameter::registrar;

I've created Base class, and two inherited classes (WithParameter and NoParameter). Both derived classes have static registrar object, which automatically adds instances of the classes to the collection.
Unfortunately, constructor parameters (if any), have to be compile time constants. But in my case, it's not a problem.
Now, we can e.g. enumerate our collection:
for (auto x : Base::registry())
{
  x.second->print_myself();
}

Full code with example can be found on my github's gist [1].

Links
[1] https://gist.github.com/loganek/c0c8dfb30cd72a5a1a47b3701b61d3da