避免在字符串中使用if-else分支来键入调度

时间:2017-12-29 16:53:39

标签: c++ enums c++14 boost-preprocessor

通常当您编写接受参数的CLI工具时,您必须处理它们。大多数情况下,您希望根据参数的值在行为之间切换。

以下是一个常见用例,程序接受一个类型,然后根据该类型打印一些内容。我正在使用Boost预处理并自动生成整个if-else分支。 这在可维护性方面非常好,因为我只需要在引入新类型时更新define。另一方面,它远非现代和优雅。

我考虑使用better-enums来避免使用if-else使用_from_string实用程序函数将字符串转换为枚举。但是,从枚举到类型的方式对我来说仍然模糊不清。

有关如何保持当前实现的良好可维护性但避免使用预处理器和宏功能的任何建议吗?

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

}

https://wandbox.org/permlink/60bAwoqYxzU1EUdw

的工作示例

2 个答案:

答案 0 :(得分:3)

另一种方法是使用普通数组而不是std::find_if代替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);
}

答案 1 :(得分:1)

您不需要使用预处理器来存储任意类型的列表并为它们生成代码。我们可以使用可变参数模板编译时字符串。您可以将预处理器使用与生成的名称和类型对隔离开来。

首先,让我们为编译时字符序列定义一个包装器。请注意,_cs文字的使用是非标准的,但在每个主要编译器中都可用,并且可能是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 {}; }

然后我们可以定义一个存储一对名称和类型的空类型:

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

一个宏可以方便地实例化它:

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

您现在可以使用空的可变参数模板类来存储您的类型:

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

现在,让我们看看main应该如何看待:

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

以上内容将打印出"float"。我们可以通过解压缩handle类型列表并使用 fold表达式来查找匹配的类型名称来实现named_type

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

检查std::stringct_str之间的相等性是令人讨厌的,但可行:

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

请注意,此答案使用C ++ 17 fold expressions。您可以使用以下技术之一在C ++ 14中替换它们:

  • 递归可变参数模板函数,其中基本案例返回默认累积值,递归案例在尾部和头部之间执行操作。

  • C ++ 11包扩展技巧,例如for_each_argument

调度发生短路:

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

由于f表达式和, true运算符,此折叠表达式将在||的第一次调用时停止。

empirical proof on wandbox.org