簡體   English   中英

返回保存在 std::variant 映射中的 std::function

[英]Returning std::function held in a map of std::variant

我有一張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]);    
}

第一部分是只有在參數匹配時才能調用函數:

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是(除了歌曲參考)一種能夠假裝我們可以調用任何東西的方式。 這就是當Rvoidnothing_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 有一個編譯時常量傳遞給它。 index_t<I>的編譯器中,您可以constexpr通過它隱含的operator std::size_tindex_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::function記錄它們的調用。

第三行顯示哪些調用成功——0 參數調用void()和 1 參數調用int(int)

最后 4 行是存儲的結果。 第一個, optional<variant>參與並持有void_t 第二次和第三次調用失敗所以nullopt ,最后一個包含將1傳遞給返回1+1的函數的結果。

活生生的例子

從返回值中,您可以看到調用是否有效(查看外部可選是否被占用),確定調用的是哪個回調(變體索引),並獲取被調用變體的值(對其進行訪問) )。


如果函數類型的數量很大,則應考慮進行優化。

上面有兩個嵌套的std::visits變量索引,都保證返回相同的值。 這意味着正在生成 O(n^2) 代碼,其中只需要 O(n) ,其中 n 是function_t的替代數量。

您可以通過將變體索引“向下”作為額外參數傳遞給call_maybevar_opt_flip來清理它。 理論上,編譯器可以計算出其他 n^2-n 生成的代碼元素是不可訪問的,但這兩者都需要編譯器進行大量工作,即使它工作也很脆弱。

這樣做會減少構建時間(這種愚蠢的行為會花費構建時間;不要在通常包含的公共頭文件中調用它!),並且可以減少運行時可執行文件的大小。

大多數編程語言和 C++ 的大多數用途不允許 O(n) 代碼生成多於 O(n) 的二進制文件; 但是模板足夠強大,特別是 std 變體,可以生成 O(n^2) 甚至 O(n^3) 二進制代碼輸出。 所以應該注意一些。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM