【问题标题】:Returning std::function held in a map of std::variant返回保存在 std::variant 映射中的 std::function
【发布时间】:2023-03-19 23:22:01
【问题描述】:

我有一张std::variant 的地图,里面有几个std::function 专业,比如:

// note the different return types
using function_t = std::variant<std::function<int(void)>, std::function<void(int)>>;
std::map<int, function_t> callbacks;
callbacks[0] = [](){ return 9; };

我如何编写一个caller(...) 辅助函数,它可以为我提供在索引中保存在我的变体中的映射std::function 的引用,允许调用类似于:

int value = caller(callbacks, 0)();

一个简单的访问者不起作用,因为function_t 中的返回类型不同,即:

// cannot compile
auto caller(std::map<int, function_t> callbacks, int idx) {
    return std::visit([](const auto& arg) { return arg; }, callbacks[idx]);    
}

【问题讨论】:

  • 没有一种类型可以包含所有这些std::function 变体。所以没有一种可能的返回类型。给定所有各种参数的联合,有一种方法可以调用存储在变体中的函数,但这与返回一些可调用对象不同。另外,你打算如何调用一个接受整数而不提供整数参数的函数?
  • @NicolBolas 你是什么意思没有包含所有这些 variations 的类型? function_t 做到了。它是函数的变体,包含变体,在这种特殊情况下,函数类型的 variations....
  • @Yakk-AdamNevraumont:好的,没有 callable 类型可以做到这一点。
  • @NicolBolas 我不明白为什么不能有这样的类型?我的意思是,std 中没有一个,OP 的问题中也没有一个。我的意思是,使用overloaded 技巧并执行overloaded&lt; std::function&lt;Sig&gt;... &gt;,您就有了一个包含所有这些变体的类型。如果它分派到错误的函数,它甚至会很好地抛出。现在,我认为在这里使用重载解析是错误的,因此我在下面的回答没有;但是扩展下面的答案以从失败时抛出/返回 nullopt 的函数变体生成可调用类型并不棘手。

标签: c++ c++17 c++20


【解决方案1】:

第一部分是只有在参数匹配时才能调用函数:

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&lt;optional&lt;Ts&gt;...&gt; 并生成optional&lt;variant&lt;Ts...&gt;&gt;,即使有重复的类型。

我们现在需要能够构建正确的返回值。

现在我们可以写这个,一个接受函数和参数变体的函数,并且可能调用活动的:

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 是(除了歌曲参考之外)一种能够假装我们可以调用任何东西的方式。这就是当Rvoidnothing_t 可能有用的地方。

variant_index_t 是我用来将变体作为泛型总和类型处理的技巧,其中可能包含重复类型。

首先我们定义一个编译时间整数,称为index。它基于现有的std::integral_constant

然后我们对它们进行变体,使得替代 3 是编译时索引 3。

然后我们可以使用std::visit( [&amp;](auto I){/*...*/}, get_variant_index(var) ) 将变体的索引用作编译时间常数。

如果 var 有 4 个备选方案并持有备选方案 2,则 get_variant_index 返回一个 std::variant&lt;index&lt;0&gt;, index&lt;1&gt;, index&lt;2&gt;, index&lt;3&gt;&gt;,其中填充了 index&lt;2&gt;

(在运行时,这很可能由一个 64 位整数 2 表示。我觉得这很有趣。)

当我们 std::visit 这个 variant_index 时,我们传递的 lambda 得到传递 index_t&lt;I&gt;。所以 lambda 有一个编译时间常量传递给它。在不笨的编译器中,您可以 constexpr 通过它隐含的 operator std::size_tindex_t&lt;I&gt; 中提取值。对于哑编译器,您必须使用std::decay_t&lt;decltype(I)&gt;::value,这将是相同的编译时间整数。

使用 那个 编译时整数,我们可以std::get&lt;I&gt;(var) lambda 内的值(并保证一个在正确的位置),我们可以用它来构造 另一个 同一个替代方案的变体,即使该其他变体具有模棱两可的替代方案。在你的情况下,你会看到,如果你有

std::function<int(int)>
std::function<int(int,int)>

“结果的变体”看起来像 std::variant&lt;int,int&gt; -- 这与 std::variant&lt;int&gt; 不同。

(作为附加步骤,您可以从此变体中删除重复的类型,但我建议单独执行此操作)

每个call_me_maybe 调用都返回一个optional&lt;R&gt;。但是variant&lt;optional&lt;R&gt;...&gt;哑巴,所以我将其转为optional&lt;variant&lt;R&gt;...&gt;

这意味着您可以快速检查函数调用是否有效,如果有效,您可以查看从中获得了什么价值。


测试代码:

    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&lt;variant&gt; 订婚并持有void_t。第二次和第三次调用失败所以nullopt,最后一个包含将1 传递给返回1+1 的函数的结果。

Live example.

从返回值可以看出调用是否有效(看是否调用了外部可选项),确定调用了哪个回调(如果调用了)(变体索引),并获取被调用变体的值(做一个访问它)。


如果函数类型的数量很大,则应该考虑进行优化。

上面有两个嵌套的变量索引std::visits,都保证返回相同的值。这意味着在只需要 O(n) 的情况下生成 O(n^2) 代码,其中 n 是 function_t 中的备选数量。

您可以通过将变体索引“向下”传递给 call_maybevar_opt_flip 作为额外参数来清除它。理论上,编译器可以计算出其他 n^2-n 生成的代码元素是不可访问的,但这两者都需要编译器进行大量工作,并且即使它工作也会很脆弱。

这样做会减少构建时间(这种愚蠢的做法会花费构建时间;不要在通常包含的公共标头中调用它!),并且可能会减少运行时可执行文件的大小。

大多数编程语言和 C++ 的大多数用途都不允许 O(n) 代码生成超过 O(n) 的二进制文件;但是模板足够强大,尤其是 std 变体,可以生成 O(n^2) 甚至 O(n^3) 二进制代码输出。所以需要注意一些。

【讨论】:

  • void 映射到nullptr,如果不可调用,总是直接返回std::nullopt,第一部分你会得到更简单、更通用的东西:&lt;class... Ts&gt; auto try_call(Ts&amp;&amp;... ts) noexcept(!std::is_invocable_v&lt;Ts...&gt; || std::is_nothrow_invocable_v&lt;Ts...&gt;) { if constexpr (!std::is_invocable_v&lt;Ts...&gt;) return std::nullopt; else if constexpr (std::is_same_v&lt;void, std::invoke_result_t&lt;Ts...&gt;&gt;) return std::invoke(std::forward&lt;Ts&gt;(ts)...), std::optional(nullptr); else return std::optional(std::invoke(std::forward&lt;Ts&gt;(ts)...)); }
  • 这太疯狂了。感谢您的时间、精力和彻底性。
  • 我知道我的问题没有简单的解决方案,但是你写这篇文章所需的流利程度和时间却是另一回事。我最终得到了一些不太通用但可维护的东西,这让我想知道 - 你会在你的团队的代码库中允许这样的代码吗?
  • @Fluffy 不,我不允许在我的代码库中映射到函数的变体。 :) 类型安全太有用了,不能以这种方式抛弃,即使我可以将它包装成像上面这样的可憎之物并使其在技术上是类型安全的。如果我不得不使用上述的可憎之物,我希望(a)我有更多的时间来打磨它,并且(b)我会修改要求以使其不那么可憎。关键的一件事是研究 Haskell 和类似语言,这种代码更自然,以使 C++ 代码更好。
  • @Yakk-AdamNevraumont 老实说,我自己完全摆脱了它,一开始就不需要异构地图,把所有东西都放下来就很清楚了。设计有缺陷,代码也有缺陷。现在我只需要找到一种方法,让我一遍又一遍地思考这些想法。无论如何,谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多