为一组函数优雅地切换模板参数



我正在编写一些使用外部库的代码,其中几个函数的定义大致如下:

// Library.h
template<typename T>
void foo(int arg1, bool arg2);
template<typename T>
int bar(float arg);

(举例说明参数列表和返回值类型都是不同的,但不包含模板类型T(。

在我的代码中,我希望能够根据一些内部映射逻辑调用foobar的不同模板实例。例如,这可以是来自表示数据类型的枚举的映射,但重要的是,此逻辑与foobar或此库中的任何其他逻辑相同。

实现这一点的一个简单方法是类似

// MyCode.h
enum class MyType { BOOL, CHAR };
void foo_wrapper(MyType type, int arg1, bool arg2)
{
if (type == MyType::BOOL)
return foo<bool>(arg1, arg2);
else if (type == MyType::CHAR)
return foo<char>(arg1, arg2);
else
throw std::runtime_error("oops");
}
int bar_wrapper(MyType type, float arg)
{
if (type == MyType::BOOL)
return bar<bool>(arg);
else if (type == MyType::CHAR)
return bar<char>(arg);
else
throw std::runtime_error("oops");
}

然而,当另一个函数需要它时,这是大量的逻辑重复更正arg名称等,留下了很多遗漏的可能性。我目前的解决方案是在每个包装器函数中都有一个相关模板实例化的静态映射:

void foo_wrapper(MyType type, int arg1, bool arg2)
{
using FunctionType = std::function<void(int, bool)>;
static const std::unordered_map<MyType, FunctionType> functionMap{
{BOOL, foo<bool>}, 
{CHAR, foo<char>}
};
if (!functionMap.count(type))
throw std::runtime_error("oops");
return functionMap.at(type)(arg1, arg2);
}
int bar_wrapper(MyType type, float arg)
{
using FunctionType = std::function<int(float)>;
static const std::unordered_map<MyType, FunctionType> functionMap{
{BOOL, bar<bool>}, 
{CHAR, bar<char>}
};
if (!functionMap.count(type))
throw std::runtime_error("oops");
return functionMap.at(type)(arg);
}

好处:参数只在代码中的一个位置传递,映射为";"集中式";在每个包装器的开头,而不是分布在包装器函数代码中。此外,被复制的选择逻辑代码也越来越少。

但是:我们仍然需要在多个包装器中复制映射对应关系,现在是映射声明的形式(想象一下以这种方式使用的十几个库函数…(

理想情况下,我希望实现一个神奇的switch_type_for_func,它可以做一些类似的事情

void foo_wrapper(MyType type, int arg1, bool arg2)
{
return switch_type_for_func<foo>(type, arg1, arg2);
}
int bar_wrapper(MyType type, float arg)
{
return switch_type_for_func<bar>(type, arg);
}

我看到这是不可行的,因为foo是一个模板,但直觉上感觉应该有一些解决方案来消除这种情况下的代码重复。

我几乎可以想象一个宏在做这项工作(因为我需要的只是函数的名称,而不是更多(,但AFAIU这些并不是最佳实践。。。也许我只是陷入了思考的困境,还有一些更合适的事情。感谢您的任何反馈/建议!

将枚举值转换为std::type_identity<T>,并将其放入可传导上下文中。CCD_ 9、CCD_。这个解决方案比我以前的解决方案优雅得多。

这需要C++20的支持,如果您实现自己的std::type_identity,这很简单,C++17就足够了。

using VType = std::variant<std::type_identity<bool>,
std::type_identity<char>>;
static const std::map<MyType, VType> dispatcher = {
{MyType::BOOL, std::type_identity<bool>{}},
{MyType::CHAR, std::type_identity<char>{}}
};
void foo_wrapper(MyType type, int arg1, bool arg2)
{
return std::visit([&](auto v){
foo<typename decltype(v)::type>(arg1, arg2);
}, dispatcher.at(type));
}

演示

首先,很明显,没有100%优雅的解决方案,因为除了实例化函数模板之外,您无法真正使用它们做任何事情。你无论如何都不能抽象它们。因此,每个函数模板至少需要少量的样板代码。

但我认为lambdas提供了一种巧妙的方法,可以用尽可能少的打字开销来实现这一点。在C++20中,你可以这样做:

template <class F>
auto dispatcher(F&& f, MyType t) {
switch (t) {
case MyType::BOOL:
return f.template operator()<bool>();
case MyType::CHAR:
return f.template operator()<char>();
};
}
auto foo_wrapper(MyType type, int arg1, bool arg2) {
return dispatcher([&]<class T>() { return foo<T>(arg1, arg2); }, type);
}
auto bar_wrapper(MyType type, float arg) {
return dispatcher([&]<class T>() { return bar<T>(arg); }, type);
}

https://godbolt.org/z/796xETr3o

这是每个新的MyType值两行,每个要包装的函数一个lambda(在您无论如何都要编写的包装函数内部(。我认为你不会比这更好的。

默认的按引用捕获应该非常好——只有当你在包装器中按值获取某些东西时,它才会失败,而原始函数通过引用获取这些东西(即使这样,它也必须将引用存储在全局状态中以引起危险(,而你显然不应该这样做。

FWIW,你可以在C++20之前(但在C++14中(使用多态lambdas来模拟它,它只是更丑陋(如果你不想完全粗糙(:

// ...
case MyType::BOOL:
return f(std::type_identity<bool>{});
// ...
return dispatcher([&](auto ti) { return foo<decltype(ti)::T>(arg1, arg2); }, type);

https://godbolt.org/z/7cjdzx681

键入/解密有点尴尬,但不需要模板化的lambda。

您可以通过在C++23:中使用std::optional<T>::or_elsestd::optional<T>定义重载的operator or来实现具有fold表达式的调度

#include <optional>
#include <type_traits>
#include <utility>
#include <variant>
template <auto Key, class T>
struct pair {};
template <class... Pairs>
struct table {};
template <class Optional, class E, E Key, class T>
constexpr Optional try_dispatch(E key, pair<Key, T>) {
return key == Key ? Optional{std::type_identity<T>{}} : std::nullopt;
}
template <class T>
constexpr std::optional<T> operator or(std::optional<T> lhs,
std::optional<T> rhs) {
return lhs.or_else([=] { return rhs; });
}
template <class F, class E, E... Keys, class... Ts>
constexpr auto dispatch(F &&f, E key, table<pair<Keys, Ts>...>) {
using Optional = std::optional<std::variant<std::type_identity<Ts>...>>;
const auto variant =
(... or try_dispatch<Optional>(key, pair<Keys, Ts>{})).value();
return std::visit(std::forward<F>(f), variant);
}
template <class T>
void foo(int arg1, bool arg2);
template <class T>
int bar(float arg);
enum class MyType { BOOL, CHAR };
constexpr auto my_table = table<pair<MyType::BOOL, bool>,
pair<MyType::CHAR, char>>{};
void foo_wrapper(MyType key, int arg1, bool arg2) {
dispatch(
[=]<class T>(std::type_identity<T>) {
foo<T>(arg1, arg2);
},
key, my_table);
}
int bar_wrapper(MyType key, float arg) {
return dispatch(
[=]<class T>(std::type_identity<T>) {
return bar<T>(arg);
},
key, my_table);
}

编译器资源管理器

好的,感谢@463035818_is_not_a_number关于模板模板参数和这里的线程的评论,我找到了一种解决方案,从技术上实现了不复制选择逻辑的目标,但是需要一些感觉太多的包装器代码才能使其工作(基本上是一个琐碎的结构来包装每个所需的库函数(-请参阅此处的完整代码:

template<class T>
struct Foo
{
static auto run(int arg1, bool arg2)
{
return foo<T>(arg1, arg2);
}
};
template<class T>
struct Bar
{
static auto run(float arg)
{
return bar<T>(arg);
}
};
template<template <typename> class T, typename ... Args>
auto switch_type_for_func(MyType type, Args... args)
{
if (type == MyType::BOOL)
return T<bool>::run(args...);
else if (type == MyType::CHAR)
return T<char>::run(args...);
else
throw std::runtime_error("oops");
}
void foo_wrapper(MyType type, int arg1, bool arg2)
{
return switch_type_for_func<Foo>(type, arg1, arg2);
}
int bar_wrapper(MyType type, float arg)
{
return switch_type_for_func<Bar>(type, arg);
}

稍微简化一下,我们可以将switch_type_for_func称为更漂亮的东西,并去掉foo_wrapperbar_wrapper,直接调用主代码,例如lib_helper<Bar>(type, arg);(请参阅此处的完整代码(。

如果有人看到了避免丑陋结构的方法,请添加您的想法!

最新更新