避免在字符串中分支 if-else 以类型调度



通常,当您编写接受参数的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 的工作示例

另一种方法是使用纯数组和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);
}

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

首先,让我们为编译时字符序列定义一个包装器。请注意,_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"。我们可以通过解压缩named_type类型的列表并使用折叠表达式查找匹配的类型名称来实现handle

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

最终结果在 wandbox.org 上直播


请注意,此答案使用 C++17折表达式。您可以在 C++14 中使用以下方法之一替换它们:

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

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


调度短路:

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

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

关于 wandbox.org 的经验证明

最新更新