Search code examples
c++pattern-matchingc++17template-meta-programming

Dispatching from a runtime parameter to different overloads


Suppose I have a set of types :

constexpr std::tuple<int,double,string> my_types;

a set of values to identify them:

constexpr std::array<const char*,3> my_ids = {"int","double","string"}; // const char* instead of string to be constexpr-compatible

and an overload set

template<class T> bool my_fun(my_type complex_object) { /* some treatment depending on type T */ }

I have a manually dispatching function like that:

string my_disp_fun(my_type complex_object) {
  const char* id = get_info(complex_object);
  using namespace std::string_literals;
  if (id == "int"s) {
    return my_fun<int>(complex_object);
  } else if (id == "double"s) {
    return my_fun<double>(complex_object);
  } else if (id == "string"s) {
    return my_fun<string>(complex_object);
  } else {
    throw;
  }
}

Because I see this pattern coming again and again with a different my_fun every time, I would like to replace it by something like that:

struct my_mapping {
  static constexpr std::tuple<int,double,string> my_types;
  static constexpr std::array<const char*,3> my_ids = {"int","double","string"}; // const char* instead of string to be constexpr-compatible
}

string my_disp_fun(my_type complex_object) {
  const char* id = get_info(complex_object);
  return
    dispatch<my_mapping>(
      id, 
      my_fun // pseudo-code since my_fun is a template
    );
}

How to implement the dispatch function? I am pretty confident it can be done but so far, I can't think of a reasonably nice API that would still be implementable with template metaprograming tricks.

I am sure people already had the need for this kind of problem. Is there a name for this pattern? I don't really know how to even qualify it in succinct technical terms...

Side question: is it related to the pattern matching proposal? I'm not sure because the paper seems more interested in the matching part, not generating branchs from that, right ?


Solution

  • Leverage variant.

    template<class T>struct tag_t{using type=T};
    template<class T>constexpr tag_t<T> tag={};
    template<class...Ts>
    using tag_enum = std::variant<tag_t<Ts>...>;
    

    now tag_enum is a type stores a type at runtime as a value. Its runtime representation is an integer (!), but C++ knows that integer represents a specific type.

    We now just have to map your strings to integers

    using supported_types=tag_enum<int, double, std::string>;
    
    std::unordered_map<std::string, supported_types> name_type_map={
      {"int", tag<int>},
      {"double", tag<double>},
      {"string", tag<std::string>},
    };
    

    this map can be built from an array and a tuple if you want, or made global somewhere, or made into a function.

    The point is, a mapping of any kind to a tag_enum can be used to auto dispatch a function.

    To see how:

    string my_disp_fun(my_type complex_object) {
      const char* id = get_info(complex_object);
      return std::visit( [&](auto tag){
        return my_fun<typename decltype(tag)::type>( complex_object );
      }, name_type_map[id] };
    }
    

    refactoring this to handle whatever level of automation you want should be easy.

    If you adopt the convention that you pass T as a tag_t as the first argument it gets even easier to refactor.

    #define RETURNS(...)\
      noexcept(noexcept(__VA_ARGS__)) \
      -> decltype(__VA_ARGS__) \
      { return __VA_ARGS__; }
    
    #define MAKE_CALLER_OF(...) \
      [](auto&&...args) \
      RETURNS( (__VA_ARGS__)(decltype(args)(args)...) )
    

    now you can easily wrap a template function into an object

    template<class F>
    auto my_disp_fun(my_type complex_object F f) {
      const char* id = get_info(complex_object);
      return std::visit( [&](auto tag){
        return f( tag, complex_object );
      }, name_type_map[id] }; // todo: handle failure to find it
    }
    

    then

    std::string s = my_disp_fun(obj, MAKE_CALLER_OF(my_fun));
    

    does the dispatch for you.

    (In theory we could pass the template parameter in the macro, but the above macros are generically useful, while one that did wierd tag unpacking are not).

    Also we can make a global type map.

    template<class T>
    using type_entry = std::pair<std::string, tag_t<T>>;
    #define TYPE_ENTRY_EX(NAME, X) type_entry<X>{ NAME, tag<X> }
    #define TYPE_ENTRY(X) TYPE_ENTRY_EX(#X, X)
    
    auto TypeTable = std::make_tuple(
      TYPE_ENTRY(int),
      TYPE_ENTRY(double),
      TYPE_ENTRY_EX("string", std::string)
    );
    
    template<class Table>
    struct get_supported_types_helper;
    template<class...Ts>
    struct get_supported_types_helper<std::tuple<type_entry<Ts>...>> {
      using type = tag_enum<Ts...>;
    };
    template<class Table>
    using get_supported_types = typename get_supported_types_helper<Table>::type;
    

    From that you can do things like make the unordered map from the TypeTable tuple automatically.

    All of this is just to avoid having to mention the supported types twice.