第一部分是只有在参数匹配时才能调用函数:
struct void_t {};
template<class R, class...Args, class...Ts,
// in C++20 do requires
std::enable_if_t<sizeof...(Args)==sizeof...(Ts), bool> = true,
class R0=std::conditional_t< std::is_same_v<R,void>, void_t, R >
>
std::optional<R0> call_me_maybe( std::function<R(Args...)> const& f, Ts&&...ts ) {
if constexpr ( (std::is_convertible_v<Ts&&, Args> && ... ))
{
if constexpr (std::is_same_v<R, void>) {
f(std::forward<Ts>(ts)...);
return void_t{};
} else {
return f(std::forward<Ts>(ts)...);
}
}
else
{
return std::nullopt;
}
}
template<class R, class...Args, class...Ts,
// in C++20 do requires
std::enable_if_t<sizeof...(Args)!=sizeof...(Ts), bool> = true,
class R0=std::conditional_t< std::is_same_v<R,void>, void_t, R >
>
constexpr std::optional<R0> call_me_maybe( std::function<R(Args...)> const& f, Ts&&...ts ) {
return std::nullopt;
}
第二部分涉及一些变体的工作:
template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index = {};
template<std::size_t...Is>
using variant_index_t = std::variant< index_t<Is>... >;
template<std::size_t...Is, class R=variant_index_t<Is...>>
constexpr R make_variant_index( std::size_t I, std::index_sequence<Is...> ) {
constexpr R retvals[] = {
R( index<Is> )...
};
return retvals[I];
}
template<std::size_t N>
constexpr auto make_variant_index( std::size_t I ) {
return make_variant_index( I, std::make_index_sequence<N>{} );
}
template<class...Ts>
constexpr auto get_variant_index( std::variant<Ts...> const& v ) {
return make_variant_index<sizeof...(Ts)>( v.index() );
}
这让您能够以对编译时更友好的方式使用变体索引。
template<class...Ts>
std::optional<std::variant<Ts...>> var_opt_flip( std::variant<std::optional<Ts>...> const& var ) {
return std::visit( [&](auto I)->std::optional<std::variant<Ts...>> {
if (std::get<I>(var))
return std::variant<Ts...>(std::in_place_index_t<I>{}, *std::get<I>(var));
else
return std::nullopt;
}, get_variant_index(var) );
}
这让我们可以获取variant<optional<Ts>...> 并生成optional<variant<Ts...>>,即使有重复的类型。
我们现在需要能够构建正确的返回值。
现在我们可以写这个,一个接受函数和参数变体的函数,并且可能调用活动的:
template<class...Sigs, class...Ts>
auto call_maybe( std::variant<std::function<Sigs>...> const& vf, Ts&&...ts )
{
using R0 = std::variant< decltype(call_me_maybe(std::function<Sigs>{}, std::forward<Ts>(ts)...))... >;
R0 retval = std::visit(
[&](auto I)->R0 {
return R0( std::in_place_index_t<I>{}, call_me_maybe(std::get<I>(vf), std::forward<Ts>(ts)... ) );
},
get_variant_index(vf)
);
return var_opt_flip( std::move(retval) );
}
然后我们重写caller来使用它:
using function_t = std::variant< std::function< void() >, std::function< int(int) > >;
template<class...Ts>
auto caller(std::map<int, function_t> const& callbacks, int idx, Ts&&...ts) {
auto it = callbacks.find(idx);
using R = decltype(call_maybe( it->second, std::forward<Ts>(ts)... ));
// wrong index:
if (it == callbacks.end())
return R(std::nullopt);
// ok, give it a try:
return call_maybe( it->second, std::forward<Ts>(ts)... );
}
会有一些编译器不喜欢我对auto I 所做的事情;在这些方面,decltype(I)::value 替换 I 可能会有所帮助(我能说什么,并非所有编译器都兼容 C++)。
基本思想是,我们为函数的可能返回值创建一个具有匹配索引的变体。然后我们返回其中一个可选的,以处理失败绝对是可能的事实(在运行时)。
call_me_maybe 是(除了歌曲参考之外)一种能够假装我们可以调用任何东西的方式。这就是当R 是void 时nothing_t 可能有用的地方。
variant_index_t 是我用来将变体作为泛型总和类型处理的技巧,其中可能包含重复类型。
首先我们定义一个编译时间整数,称为index。它基于现有的std::integral_constant。
然后我们对它们进行变体,使得替代 3 是编译时索引 3。
然后我们可以使用std::visit( [&](auto I){/*...*/}, get_variant_index(var) ) 将变体的索引用作编译时间常数。
如果 var 有 4 个备选方案并持有备选方案 2,则 get_variant_index 返回一个 std::variant<index<0>, index<1>, index<2>, index<3>>,其中填充了 index<2>。
(在运行时,这很可能由一个 64 位整数 2 表示。我觉得这很有趣。)
当我们 std::visit 这个 variant_index 时,我们传递的 lambda 得到传递 index_t<I>。所以 lambda 有一个编译时间常量传递给它。在不笨的编译器中,您可以 constexpr 通过它隐含的 operator std::size_t 从 index_t<I> 中提取值。对于哑编译器,您必须使用std::decay_t<decltype(I)>::value,这将是相同的编译时间整数。
使用 那个 编译时整数,我们可以std::get<I>(var) lambda 内的值(并保证一个在正确的位置),我们可以用它来构造 另一个 同一个替代方案的变体,即使该其他变体具有模棱两可的替代方案。在你的情况下,你会看到,如果你有
std::function<int(int)>
std::function<int(int,int)>
“结果的变体”看起来像 std::variant<int,int> -- 这与 std::variant<int> 不同。
(作为附加步骤,您可以从此变体中删除重复的类型,但我建议单独执行此操作)
每个call_me_maybe 调用都返回一个optional<R>。但是variant<optional<R>...> 是哑巴,所以我将其转为optional<variant<R>...>。
这意味着您可以快速检查函数调用是否有效,如果有效,您可以查看从中获得了什么价值。
测试代码:
std::map<int, function_t> callbacks = {
{ 0, []{ std::cout << 0 << "\n"; } },
{ 1, [](int x){ std::cout << "1:" << x << "\n"; return x+1; } },
};
std::optional<std::variant<void_t, int>> results[] = {
caller(callbacks, 0),
caller(callbacks, 0, 1),
caller(callbacks, 1),
caller(callbacks, 1, 1),
};
for (auto&& op:results) {
std::cout << (bool)op;
}
std::cout << "\n";
auto printer = [](auto val) {
if constexpr (std::is_same_v<decltype(val), void_t>) {
std::cout << "void_t";
} else {
std::cout << val;
}
};
int count = 0;
for (auto&& op:results) {
std::cout << count << ":";
if (!op) {
std::cout << "nullopt\n";
} else {
std::visit( printer, *op );
std::cout << "\n";
}
++count;
}
我得到这个输出:
0
1:1
1001
0:void_t
1:nullopt
2:nullopt
3:2
前两行是 void() 和 int(int) std::functions 记录他们的呼叫。
第三行显示成功的调用——第 0 个参数调用 void() 和第 1 个参数调用 int(int)。
最后 4 行是存储的结果。第一个,optional<variant> 订婚并持有void_t。第二次和第三次调用失败所以nullopt,最后一个包含将1 传递给返回1+1 的函数的结果。
Live example.
从返回值可以看出调用是否有效(看是否调用了外部可选项),确定调用了哪个回调(如果调用了)(变体索引),并获取被调用变体的值(做一个访问它)。
如果函数类型的数量很大,则应该考虑进行优化。
上面有两个嵌套的变量索引std::visits,都保证返回相同的值。这意味着在只需要 O(n) 的情况下生成 O(n^2) 代码,其中 n 是 function_t 中的备选数量。
您可以通过将变体索引“向下”传递给 call_maybe 和 var_opt_flip 作为额外参数来清除它。理论上,编译器可以计算出其他 n^2-n 生成的代码元素是不可访问的,但这两者都需要编译器进行大量工作,并且即使它工作也会很脆弱。
这样做会减少构建时间(这种愚蠢的做法会花费构建时间;不要在通常包含的公共标头中调用它!),并且可能会减少运行时可执行文件的大小。
大多数编程语言和 C++ 的大多数用途都不允许 O(n) 代码生成超过 O(n) 的二进制文件;但是模板足够强大,尤其是 std 变体,可以生成 O(n^2) 甚至 O(n^3) 二进制代码输出。所以需要注意一些。