2

I have some code where I have a base class (lets call it foo) that has a variable number of derived classes (between 10-500) created by a generation script. Currently we have a function that will create a new base class by passing in its name as a string and then using a giant if/else statement to find the right one. for example

if      (name == "P2_26") {add_module(new P2_26());}  
else if (name == "P4_30") {add_module(new P4_30());}
...

This leads to a giant if else block. This seems to me like code that could be simplified by using tag dispatching, but every example I find online uses built-ins like iterators that already have tags defined and I could not interpolate to my use case. Is there anyway to streamline this code?

Prgrm.celeritas
  • 311
  • 2
  • 12
  • AFAIK tag dispatching involves having many overloads with a dummy type parameter (i.e. the tag), I'm not sure how this would be less code, and since its a compile time choice I'm not sure how that helps you here. You'd still have to list the assiation from name to function (overload) anyway. Am I misunderstanding? – Borgleader May 08 '17 at 17:24

2 Answers2

3

Tag dispatched is based on type information as an input. Judging from your code you have a string as an input which which can not be used in run time.
Your case looks more like an abstract factory:

// Factory.h
class Base;

struct Factory {
  using spawn_t = std::function<Base*()>;
  using container_t = std::unordered_map<std::string, spawn_t>;
  
  static container_t& producers() {
    // This way it will be initialized before first use
    static container_t producers;
    return producers;
  }

  static Base* spawn(const std::string& name) {
    auto it = producers().find(name);
    if (it == producers().end()) return nullptr;
    return it->second();
  }
};

// Base.h
#define STR(x) #x
#define DEFINE_REGISTRATOR(_class_) \
DerivedRegistrator<_class_> _class_::_sRegistrator_(STR(_class_))

#define DECLARE_REGISTRATOR(_class_) \
static DerivedRegistrator<_class_> _sRegistrator_

template<typename T>
struct DerivedRegistrator{
  DerivedRegistrator(const std::string& name) {
    Factory::producers()[name] = [](){ return new T(); };
  }
};

class Base {
  // ...
};

And then generated files should include:

// Derived1.h
class Derived1 : public Base {
  DECLARE_REGISTRATOR(Derived1);
  // ...
};

// Derived1.cpp
DEFINE_REGISTRATOR(Derived1); // Will register automatically

This solution will register all classes automatically on program start which is more like what you had before.


UPD.

To use it you can simply replace all your if-else code with this line:

add_module(Factory::spawn(name));

Or if you can't handle nullptr in add_module:

Base* ptr = Factory::spawn(name);
if (ptr) {
  add_module(ptr);
}

Thanks to D Drmmr for making this code better.

Teivaz
  • 5,462
  • 4
  • 37
  • 75
  • 1
    As presented, this suffers from the [static initialization order fiasco](https://isocpp.org/wiki/faq/ctors#static-init-order). You need to use a singleton for your factory (or its map of callback functions). – D Drmmr May 08 '17 at 18:24
  • @DDrmmr indeed. I need to fix it – Teivaz May 08 '17 at 18:28
  • @teivaz I am still a little confused, what would I pass to the add_modules function? – Prgrm.celeritas May 08 '17 at 19:01
2
template<class T>
struct named_factory {
  const char* name;
  std::function<std::unique_ptr<T>()> factory;
};
struct find_factory {
  using is_transparent=std::true_type;
  struct named {
    const char* str;
    template<class T>
    named(named_factory<T> const& f):str(f.name) {}
    named(const char* name):str(name) {}
  };
  bool operator()(named lhs, named rhs) {
    return strcmp(lhs.str, rhs.str)<0;
  }
};
#define MAKE_STR2(X) #X
#define MAKE_STR(X) MAKE_STR2(X)
#define FACTORY(X,...) \
  named_factory<__VA_ARGS__>{\
    MAKE_STR(X),\
    []{\
      return std::make_unique<X>()\
    }\
  }

Now we can:

std::set<named_factory<foo>, find_factory> factories = {
  FACTORY(P2_26, foo),
  FACTORY(P4_30, foo),
  // ...
};

and in code you do:

bool add_module_by_name( const char* name ) {
  auto it = factories.find(name);
  if (it == factories.end()) return false;
  auto module = it->factory();
  if (!module) return false;
  add_module( module.release() );
  return true;
}

This is a data-driven design. The search for the right type is done in logarithmic time, not linear like your code. You could probably replace it with an unordered_map instead of a set.


However, if your type names are determined at compile time, you can do better. (Ie, if you have a hard coded "P2_26" at the call site).

template<class T>
struct tag_t { using type=T; constexpr tag_t(){} };
template<class T>
constexpr tag_t<T> tag{};

template<class T>
void add_module( tag_t<T> ) {
  // ...
  add_module( new T() );
}

Now you can add_module(tag<P2_26>) and skip the long if/else statement.

We can even hide the implementation of the outer add_module via this:

// in cpp file:
void add_module_impl( std::function< std::unique_ptr<foo>() > maker ) {
  // ...
  add_module( maker().release() );
}
// in h file:
void add_module_impl( std::function< std::unique_ptr<foo>() > maker );
template<class T>
void add_module( tag_t<T> t ) {
  add_module_impl([]{ return std::make_unique<T>(); });
}

and again, we can add_module(tag<P4_30>) and it just works.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • If you have the hard coded `"P2_26"` why not just call `add_module(new P2_26());`? – D Drmmr May 08 '17 at 18:29
  • what part of this code would need to added to the generated derived classes? Or is is it all external to them? (the type names are determined at run time) – Prgrm.celeritas May 08 '17 at 18:33
  • @DDrmmr that is exactly what I am doing, but there are many modules hence the giant if/else block. the derived class name is not given till run time. – Prgrm.celeritas May 08 '17 at 18:35
  • @Prgrm.celeritas Which of the solutions are you asking about? Nothing has to be added to the generated derived classes in any of them. The set solution does require that the `FACTORY(name,foo),` line be added for each derived class. I put it in one spot; you could do something fancy to distribute the registration (and put it in the derived classes). – Yakk - Adam Nevraumont May 08 '17 at 18:35
  • @Yakk I was asking if there needed to be any changes to the derived classes, since that would require more work. But you qualified that they do not need to be modified. – Prgrm.celeritas May 08 '17 at 18:38