【问题标题】:how to capture a parameter pack by forward or move?如何通过前进或移动捕获参数包?
【发布时间】:2016-05-29 14:34:02
【问题描述】:

假设我有这个功能:

template <typename ...A>
void test(A&& ...a)
{
  [=]()
  {
  };
}

参数包是要转发到 lambda 中还是只是按值复制?我很担心,因为我通常必须明确指定move()forward(),因为a... 是左值。转发/移动它们是否需要元组中介?如果是这样,是否有一种简单的方法可以将元组解压缩到参数包中,而不使用索引技巧?

【问题讨论】:

  • 如何通过引用捕获它,然后在 lambda body 中使用 forward ?按价值捕获不会为您做到这一点。
  • 另外,如果你能写一个伪代码来说明你到底想做什么,那将会很有帮助。
  • 你打算如何使用你的 lambda?另外,太糟糕了[a = (std::forward&lt;A&gt;(a)...)](){}; 无法编译。如果您不在其他任何地方使用参数,编译器可能会优化捕获。
  • 这是关于在一个线程中运行一些代码。如果我通过引用捕获,则在线程开始运行时引用可能会悬空。所以想法是通过转发复制所有内容,然后在线程内使用副本。

标签: c++ c++17


【解决方案1】:

一种方法是编写 Haskell 意义上的函子。嗯,一个可变的,不是很 Haskell。

写一个签名函数(Ts...)-&gt;( ((Ts...)-&gt;X) -&gt; X )。即一个接受一个包并返回一个函数的函数。返回的函数可以采用一个函数来获取该包并对其进行评估。

template<class...Ts>
auto make_functor(Ts&&...ts); // TODO

一旦有了,我们就可以轻松解决您的问题。

template<class ...A>
auto test(A&& ...a) {
  return [unpack_a=make_functor(std::forward<A>(a)...)]() mutable
  {
    return unpack_a([&](auto&&...a){
      // here you have access to a...
      return sizeof...(a);
    });
  };
}

test 接受一个包,并返回一个返回该包大小的函数(嗯,对包做任何事情)。

make_functor 并不容易:基本上,我们编写一个手动 lambda,将 args 存储在一个元组中,然后在运算符 () 中解压缩索引技巧。

实际上,我们在手动的伪 lambda 类中进行一次包存储和解包一次,然后可以在以后重用它。

再想一想,最好写一个延迟应用,它接受一个元组,存储它,然后再使用std::apply

template<class...Ts>
auto delayed_apply(std::tuple<Ts...> tup){
  return [tup=std::move(tup)](auto&&f)->decltype(auto) mutable{
    return std::experimental::apply(decltype(f)(f), std::move(tup));
  };
}

让参数的值/引用不会丢失!

template<class ...A>
auto test(A&& ...a) {
  return [unpack_a=delayed_apply(std::forward_as_tuple(std::forward<A>(a)...))]() mutable
  {
    return unpack_a([&](auto&&...a){
      // here you have access to a...
      return sizeof...(a);
    });
  };
}

这确实需要std::experimental::apply

如果您想存储右值并将左值作为参考:

unpack_a=delayed_apply(std::tuple<A...>(std::forward<A>(a)...))

如果你想同时存储 l 和 r 值:

unpack_a=delayed_apply(std::make_tuple(std::forward<A>(a)...))

如您所见,这种方法提供了很多控制。

如果您需要std::experimental::apply,这里有参考实现:比我在智能手机上写的任何东西都要好。

请注意,make_functor 可以写成delayed_apply,但相反的是……不正确。

如果您感到困惑,unpack_a 接受一个 lambda 并将用于创建 unpack_a 的元组解包到其中。基本上,我们将一个对象存储为整个包,然后在需要时将其解包到 lambda 的主体中。

如果您希望解包有时“不止一次”工作,有时“只工作一次”,可能需要更长的delayed_apply 来处理 const 和非 const 甚至右值重载。它必须返回一个类,而不是 lambda。恼人的。使示例代码工作,我认为,仍然没有编译。

幸好这种东西是一次写,用多次。

【讨论】:

  • 这个前锋里面有必要吗? std::forward_as_tuple(std::forward&lt;A&gt;(a)...)
  • @coy 取决于用例。有时是有时不是。
【解决方案2】:

可以使用std::bind 完成的少数有用的事情之一。捕获由bind 执行,捕获的值作为参数传递给无捕获的通用 lambda:

template <typename... A>
auto test(A&&... a)
{
    auto f = [](auto&&... a)
    {
        // use a...
    };
    return std::bind(f, std::forward<A>(a)...);
}

Live demo

以上内容适用于 Clang,但这个 GCC 似乎存在虚假 volatile 限定符的问题。

我们可以在没有 bind 的情况下做到这一点,方法是在第二个 lambda 中捕获 tuple,该 lambda 调用 std::apply (C++17) 将元组解包到第一个 lambda 的参数列表中:

template <typename... A>
auto test(A&&... a)
{
    auto f = [](auto&&... a)
    {
        // use a...
    };
    return [f, tup = std::make_tuple(std::forward<A>(a)...)]() mutable { std::apply(f, tup); };
}

Live demo

适用于 Clang 和 GCC; apply 是使用您想要避免的索引技巧实现的,但您没有接触到它。 mutable 表示第二个 lambda 的调用运算符是非常量的,因此元组元素最终不会获得 const 资格。


C++20

C++20 对完美转发捕获有适当的支持:

template <typename... A>
auto test(A&&... a)
{
    return [...a = std::forward<A>(a)]()
    {
        // use a...
    };
}

【讨论】:

  • 如果 lambda 有参数怎么办?
【解决方案3】:

首先用完美转发捕获元组中的参数:

template <typename ...A>
void test(A&& ...a)
{
  [tup= std::tuple<A...>(std::forward<A>(a)...)]()
  {
       //tup should contain the forwarded elements
  };
}

然后使用这个答案:https://stackoverflow.com/a/7858971/835629 在以后的函数调用中解包元组。

//utils
template<int ...>
struct seq { };

template<int N, int ...S>
struct gens : gens<N-1, N-1, S...> { };

template<int ...S>
struct gens<0, S...> {
  typedef seq<S...> type;
};


template<typename F, typename T, int ...S>
void unpackTupleToFunction_utils(F func, const T &tup, seq<S...>) {
 func(std::get<S>(tup) ...);
}

template<typename F, typename ...Args, int ...S>
void unpackTupleToFunction(F func, const std::tuple<Args...> &tup) {
 unpackTupleToFunction_utils(func, tup, typename gens<sizeof...(Args)>::type());
}

最后解压 lambda 中的元组,以便用它调用函数:

template <typename ...Args>
void test(Args&& ...a) {
  auto lambda = [tup= std::tuple<Args...>(std::forward<Args>(a)...)]()
  {
    unpackTupleToFunction(f, tup);
  };

  lambda();
  lambda();
  lambda();
}

PS:很遗憾像[a = (std::forward&lt;Args&gt;(a)...)](){}; 这样的东西不能编译。

【讨论】:

  • 是的,是的,但索引技巧是我试图避免的,不过你是完全正确的。
【解决方案4】:

为什么不按值传递?只转发到顶层函数。

假设你传递了一个 intstd::string&amp;float&amp;&amp; 例如,所以你的函数看起来像

void test(int,string&,float&&)
{
  [=]()
  {
  };
}

从那里,匿名 lambda 将按值复制intstring&amp;float&amp;&amp;。引用的副本仍然是副本。 您可以使用 tuple 再次打包参数并将它们解压缩到 lambda 中。

如何在 lamda 中使用元组?

  1. 像我们通常使用可变参数模板一样使用递归
  2. 找到std::apply 的一些非标准实现并将元组用作anotehr 函数的参数

【讨论】:

  • 是的,但是如何在没有get&lt;&gt;()的索引的情况下解压
  • 引用的副本在此上下文中是一个值,不再是引用。而且这个副本是通过复制而不是移动生成的,即使是从右值。
猜你喜欢
  • 2021-04-07
  • 1970-01-01
  • 1970-01-01
  • 2015-10-13
  • 1970-01-01
  • 1970-01-01
  • 2021-04-12
  • 2020-12-26
  • 2014-02-09
相关资源
最近更新 更多