c ++ lambdas如何从上层范围捕获可变参数包

Posted

技术标签:

【中文标题】c ++ lambdas如何从上层范围捕获可变参数包【英文标题】:c++ lambdas how to capture variadic parameter pack from the upper scope 【发布时间】:2018-05-09 19:57:14 【问题描述】:

我研究了通用 lambda,并对示例稍作修改, 所以我的 lambda 应该捕获上层 lambda 的可变参数包。 所以基本上作为(auto&&...) 给上层lambda 的内容应该以某种方式在[=] 块中捕获。

(完美转发是另一个问题,我很好奇这里有可能吗?)

#include <iostream>
#include<type_traits>
#include<utility>


// base case
void doPrint(std::ostream& out) 

template <typename T, typename... Args>
void doPrint(std::ostream& out, T && t, Args && ... args)

    out << t << " ";                // add comma here, see below
    doPrint(out, std::forward<Args&&>(args)...);


int main()

    // generic lambda, operator() is a template with one parameter
    auto vglambda = [](auto printer) 
        return [=](auto&&... ts) // generic lambda, ts is a parameter pack
        
            printer(std::forward<decltype(ts)>(ts)...);
            return [=]   // HOW TO capture the variadic ts to be accessible HERE ↓
                printer(std::forward<decltype(ts)>(ts)...); // ERROR: no matchin function call to forward
            ; // nullary lambda (takes no parameters)
        ;
    ;
    auto p = vglambda([](auto&&...vars) 
        doPrint(std::cout, std::forward<decltype(vars)>(vars)...);
    );
    auto q = p(1, 'a', 3.14,5); // outputs 1a3.14

    //q(); //use the returned lambda "printer"


【问题讨论】:

旁白:你可能不想转发同一个包两次 【参考方案1】:

C++20 中的完美捕捉

template <typename ... Args>
auto f(Args&& ... args)
    return [... args = std::forward<Args>(args)]
        // use args
    ;


C++17 和 C++14 解决方法

在 C++17 中,我们可以使用元组的解决方法:

template <typename ... Args>
auto f(Args&& ... args)
    return [args = std::make_tuple(std::forward<Args>(args) ...)]()mutable
        return std::apply([](auto&& ... args)
            // use args
        , std::move(args));
    ;

不幸的是 std::apply 是 C++17,在 C++14 中你可以自己实现它或者用 boost::hana 做类似的事情:

namespace hana = boost::hana;

template <typename ... Args>
auto f(Args&& ... args)
    return [args = hana::make_tuple(std::forward<Args>(args) ...)]()mutable
        return hana::unpack(std::move(args), [](auto&& ... args)
            // use args
        );
    ;

通过函数capture_call 简化解决方法可能很有用:

#include <tuple>

// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args)
    return [
        lambda = std::forward<Lambda>(lambda),
        capture_args = std::make_tuple(std::forward<Args>(args) ...)
    ](auto&& ... original_args)mutable
        return std::apply([&lambda](auto&& ... args)
            lambda(std::forward<decltype(args)>(args) ...);
        , std::tuple_cat(
            std::forward_as_tuple(original_args ...),
            std::apply([](auto&& ... args)
                return std::forward_as_tuple< Args ... >(
                    std::move(args) ...);
            , std::move(capture_args))
        ));
    ;

像这样使用它:

#include <iostream>

// returns a callable object without parameters
template <typename ... Args>
auto f1(Args&& ... args)
    return capture_call([](auto&& ... args)
        // args are perfect captured here
        // print captured args via C++17 fold expression
        (std::cout << ... << args) << '\n';
    , std::forward<Args>(args) ...);


// returns a callable object with two int parameters
template <typename ... Args>
auto f2(Args&& ... args)
    return capture_call([](int param1, int param2, auto&& ... args)
        // args are perfect captured here
        std::cout << param1 << param2;
        (std::cout << ... << args) << '\n';
    , std::forward<Args>(args) ...);


int main()
    f1(1, 2, 3)();     // Call lambda without arguments
    f2(3, 4, 5)(1, 2); // Call lambda with 2 int arguments


这是capture_call的C++14实现:

#include <tuple>

// Implementation detail of a simplified std::apply from C++17
template < typename F, typename Tuple, std::size_t ... I >
constexpr decltype(auto)
apply_impl(F&& f, Tuple&& t, std::index_sequence< I ... >)
    return static_cast< F&& >(f)(std::get< I >(static_cast< Tuple&& >(t)) ...);


// Implementation of a simplified std::apply from C++17
template < typename F, typename Tuple >
constexpr decltype(auto) apply(F&& f, Tuple&& t)
    return apply_impl(
        static_cast< F&& >(f), static_cast< Tuple&& >(t),
        std::make_index_sequence< std::tuple_size<
            std::remove_reference_t< Tuple > >::value >);


// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args)
    return [
        lambda = std::forward<Lambda>(lambda),
        capture_args = std::make_tuple(std::forward<Args>(args) ...)
    ](auto&& ... original_args)mutable
        return ::apply([&lambda](auto&& ... args)
            lambda(std::forward<decltype(args)>(args) ...);
        , std::tuple_cat(
            std::forward_as_tuple(original_args ...),
            ::apply([](auto&& ... args)
                return std::forward_as_tuple< Args ... >(
                    std::move(args) ...);
            , std::move(capture_args))
        ));
    ;


capture_call 按值捕获变量。完美意味着尽可能使用移动构造函数。下面是一个 C++17 代码示例,以便更好地理解:

#include <tuple>
#include <iostream>
#include <boost/type_index.hpp>


// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args)
    return [
        lambda = std::forward<Lambda>(lambda),
        capture_args = std::make_tuple(std::forward<Args>(args) ...)
    ](auto&& ... original_args)mutable
        return std::apply([&lambda](auto&& ... args)
            lambda(std::forward<decltype(args)>(args) ...);
        , std::tuple_cat(
            std::forward_as_tuple(original_args ...),
            std::apply([](auto&& ... args)
                return std::forward_as_tuple< Args ... >(
                    std::move(args) ...);
            , std::move(capture_args))
        ));
    ;


struct A
    A()
        std::cout << "  A::A()\n";
    

    A(A const&)
        std::cout << "  A::A(A const&)\n";
    

    A(A&&)
        std::cout << "  A::A(A&&)\n";
    

    ~A()
        std::cout << "  A::~A()\n";
    
;

int main()
    using boost::typeindex::type_id_with_cvr;

    A a;
    std::cout << "create object end\n\n";

    [b = a]
        std::cout << "  type of the capture value: "
          << type_id_with_cvr<decltype(b)>().pretty_name()
          << "\n";
    ();
    std::cout << "value capture end\n\n";

    [&b = a]
        std::cout << "  type of the capture value: "
          << type_id_with_cvr<decltype(b)>().pretty_name()
          << "\n";
    ();
    std::cout << "reference capture end\n\n";

    [b = std::move(a)]
        std::cout << "  type of the capture value: "
          << type_id_with_cvr<decltype(b)>().pretty_name()
          << "\n";
    ();
    std::cout << "perfect capture end\n\n";

    [b = std::move(a)]()mutable
        std::cout << "  type of the capture value: "
          << type_id_with_cvr<decltype(b)>().pretty_name()
          << "\n";
    ();
    std::cout << "perfect capture mutable lambda end\n\n";

    capture_call([](auto&& b)
        std::cout << "  type of the capture value: "
          << type_id_with_cvr<decltype(b)>().pretty_name()
          << "\n";
    , std::move(a))();
    std::cout << "capture_call perfect capture end\n\n";

输出:

  A::A()
create object end

  A::A(A const&)
  type of the capture value: A const
  A::~A()
value capture end

  type of the capture value: A&
reference capture end

  A::A(A&&)
  type of the capture value: A const
  A::~A()
perfect capture end

  A::A(A&&)
  type of the capture value: A
  A::~A()
perfect capture mutable lambda end

  A::A(A&&)
  type of the capture value: A&&
  A::~A()
capture_call perfect capture end

  A::~A()

捕获值的类型在capture_call 版本中包含&amp;&amp;,因为我们必须通过引用访问内部元组中的值,而支持的语言捕获支持直接访问该值。

【讨论】:

我对 C++17 解决方案(使用 std::make_tuple() 和 std::apply() 的解决方案)有点困惑。首先,我希望 make_tuple() 创建的元组包含 std::decay-ed 类型,除了 std::reference_wrapper 类型的参数。此外,std::apply() 会将元组转发给 std::get(),如果我们将左值引用传递给元组,我相信它将返回左值引用,否则将返回右值引用。所以总而言之,看起来我们并没有完美地转发捕获中的参数包。我错过了什么? @fireboot 捕获的对象作为值存储在 lambda 对象中。 “完美”捕获是指通过完美转发完成这些值的构建。 apply 将始终为您提供对捕获的(在存储的元组中)值的右值引用,这相当于定义了可变的 C++20 版本 lambda。我添加了一个示例以便更好地理解。多亏了你,我在原始的 capture_call 中发现了一个错误!之前的第二个 make_tuple 现在被第二个 apply 替换,它为捕获的值创建一个右值引用“tuple-view”。 为什么是 make_tuple,而不是 forward_as_tuple? C++20 解决方案不是也需要mutable 吗? Args&amp;&amp; args 应该是 Args&amp;&amp; ...args @Mr.Wonko 你对... 的看法是正确的,我已修复它。谢谢!是否需要mutable 取决于闭包中的值是否应为const。所以是的,如果你想要旧解决方法的确切行为,那么是的。【参考方案2】:

您可以使用std::bind 将可变参数绑定到您的 lambda(对于 C++20 之前的解决方案),而不是使用 std::tuplestd::apply,这会使代码变得很混乱:

template <typename... Args>
auto f(Args&&... args)

    auto functional = [](auto&&... args)  /* lambda body */ ;
    return std::bind(std::move(functional), std::forward<Args>(args)...);

【讨论】:

【参考方案3】:

完美转发是另一个问题,我很好奇这里有可能吗?

嗯...在我看来,完美的转发的问题。

ts... 的捕获效果很好,如果您在内部 lambda 中进行更改,

printer(std::forward<decltype(ts)>(ts)...);

printer(ts...);

程序编译。

问题是通过捕获ts...(使用[=])它们变成const值和printer()(即接收auto&amp;&amp;...vars的lambda)接收引用( &amp;&amp;&amp;)。

你可以用下面的函数看到同样的问题

void bar (int &&)
  

void foo (int const & i)
  bar(std::forward<decltype(i)>(i)); 

从 clang++ 我得到

tmp_003-14,gcc,clang.cpp:21:4: error: no matching function for call to 'bar'
  bar(std::forward<decltype(i)>(i)); 
   ^~~
tmp_003-14,gcc,clang.cpp:17:6: note: candidate function not viable: 1st argument
      ('const int') would lose const qualifier
void bar (int &&)
     ^

解决问题的另一种方法是将ts... 捕获为引用(因此[&amp;])而不是值。

【讨论】:

以上是关于c ++ lambdas如何从上层范围捕获可变参数包的主要内容,如果未能解决你的问题,请参考以下文章

如何在标准函数 lambda c++ 11 中正确捕获参数包

如何创建一个可变的通用lambda?

如何从 C++14 中的广义 lambda 捕获返回包含 std::unique_ptr 的 std::function?

C++11lambda表达式精讲

可变 lambda 是不是有自己的捕获值副本?

Lambda 按值捕获和“可变”关键字