Avoid if-else branching in string to type dispatching

烈酒焚心 提交于 2019-12-04 23:23:34

Another way is to use a plain array and std::find_if instead of if-else:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <typeinfo>

struct Handler {
    char const* name;
    void(*fn)(std::string const&); // Or std::function<> to accept lambdas.
};

struct A {};
struct B {};

template<class T>
void foo(std::string const& name) {
    std::cout << "foo<" << typeid(T).name() << ">: " << name << '\n';
}

int main(int, char** av) {
    Handler const handlers[] = {
          {"a", foo<A>}
        , {"b", foo<B>}
    };
    std::string const name = av[1];
    auto handler = std::find_if(std::begin(handlers), std::end(handlers), [&name](auto const& h) {
        return name == h.name;
    });
    if(handler != std::end(handlers))
        handler->fn(name);
}

You don't need to use the preprocessor to store an arbitrary list of types and generate code for them. We can use variadic templates and compile-time strings. You can isolate preprocessor usage to the generation of pairs of names and types.

Firstly, let's define a wrapper for a compile-time sequence of characters. Note that the use of the _cs literal is non-Standard, but available in every major compiler and likely to be part of C++20:

template <char... Cs>
using ct_str = std::integer_sequence<char, Cs...>;

template <typename T, T... Cs>
constexpr ct_str<Cs...> operator""_cs() { return {}; }

We can then define an empty type that stores a pair of a name and a type:

template <typename Name, typename T>
struct named_type
{
    using name = Name;
    using type = T;
};

And a macro to conveniently instantiate it:

#define NAMED_TYPE(type) \
    named_type<decltype(#type ## _cs), type>

You can now use an empty variadic template class to store your types:

template <typename... Ts>
struct named_type_list { };

using my_types = named_type_list<
    NAMED_TYPE(int),
    NAMED_TYPE(long),
    NAMED_TYPE(float),
    NAMED_TYPE(double)
>;

Now, let's see how our main should look:

int main()
{
    const std::string input{"float"};
    handle(my_types{}, input, [](auto t)
    {
        print(typename decltype(t)::name{});
    });
}

The above will print out "float". We can implement handle by unpacking the list of named_type types and using a fold expression to find the matching type name:

template <typename... Ts, typename F>
void handle(named_type_list<Ts...>, const std::string& input, F&& f)
{
    ( (same(input, typename Ts::name{}) && (f(Ts{}), true) ) || ...);
}

Checking for equality between std::string and ct_str is annoying, but doable:

template <std::size_t... Is, char... Cs>
bool same_impl(const std::string& s, 
               std::integer_sequence<char, Cs...>, 
               std::index_sequence<Is...>)
{
    return ((s[Is] == Cs) && ...);
}

template <char... Cs>
bool same(const std::string& s, std::integer_sequence<char, Cs...> seq)
{
    return s.size() >= sizeof...(Cs) 
        && same_impl(s, seq, std::make_index_sequence<sizeof...(Cs)>{});
}

final result live on wandbox.org


Note that this answer uses C++17 fold expressions. You can replace them in C++14 with one of the following techniques:

  • Recursive variadic template function, where the base case returns the default accumulation value, and the recursive case performs an operation between the tail and the head.

  • C++11 pack expansion tricks such as for_each_argument.


The dispatching does short-circuit:

( (same(input, typename Ts::name{}) && (f(Ts{}), true) ) || ...);

This fold expression will stop at the first invocation of f thanks to the , true expression and the || operator.

empirical proof on wandbox.org

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!