Avoid if-else branching in string to type dispatch

2020-08-08 07:46发布

问题:

Usually when you write a CLI tool which accepts parameter you have to deal with them. Most of the time you want to switch between behaviours based on the value of an argument.

The following is a common use case, where the program accepts a type and then prints something based on that type. I am using Boost to pre-process and auto generate the whole if-else branches. This is very nice in terms of maintainability as I only need to update a define when I introduce a new type. On the other hand it is quite far from being modern and elegant.

I thought about using better-enums to avoid using the if-else to convert from string into an enum using the _from_string utility function. But then the way to go from enum to a type is still obscure to me.

Any suggestion on how to keep the nice maintainability of the current implementation but avoid to use pre-processor and macro functionalities?

#include <iostream>
#include <cstdlib>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/preprocessor/seq/for_each.hpp>
#include <type_traits>
using a_type = int;
using b_type = long;
using c_type = float;
using d_type = double;

#define TYPES (a)(b)(c)(d)

template<typename T>
void foo(){
    T num = 1;
    std::cout << typeid(decltype(num)).name() << " : "<< num << std::endl;
};

int main(int argc, char **argv)
{
if (argc < 1) {
    return 1;
}
std::string type = argv[1];

    if (false) {
#define LOOP_BODY(R, DATA, T)                  \
    }                                          \
    else if (type == BOOST_PP_STRINGIZE(T)) {  \
        foo<BOOST_PP_CAT(T, _type)>();         \

        BOOST_PP_SEQ_FOR_EACH(LOOP_BODY, _, TYPES);
#undef LOOP_BODY
    } else {
        std::cout << "ERROR: Unknown type " << type << std::endl;
    }

}

Working example at https://wandbox.org/permlink/60bAwoqYxzU1EUdw

回答1:

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);
}


回答2:

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