c++ lambdas how to capture variadic parameter pack from the upper scope

后端 未结 3 513
梦如初夏
梦如初夏 2020-12-12 20:43

I study the generic lambdas, and slightly modified the example, so my lambda should capture the upper lambda\'s variadic parameter pack. So basically what is given to upper

相关标签:
3条回答
  • 2020-12-12 20:59

    Perfect capture in C++20

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

    C++17 and C++14 workaround

    In C++17 we can use a workaround with tuples:

    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));
        };
    }
    

    Unfortunately std::apply is C++17, in C++14 you can implement it yourself or do something similar with 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
            });
        };
    }
    

    It might be usefull to simplify the workaround by a function 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))
            ));
        };
    }
    

    Use it like this:

    #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
    }
    

    Here is a C++14 implementation of capture_call:

    #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 captures variables by value. The perfect means that the move constructor is used if possible. Here is a C++17 code example for better understanding:

    #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";
    }
    

    Output:

      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()
    

    The type of the capture value contains && in the capture_call version because we have to access the value in the internal tuple via reference, while a language supported capture supports direct access to the value.

    0 讨论(0)
  • 2020-12-12 21:05

    Instead of using std::tuple and std::apply, which clutter the code a lot, you can use std::bind to bind the variadic arguments to your lambda (for a pre-C++20 solution):

    template <typename... Args>
    auto f(Args&&... args){
    
        auto functional = [](auto&&... args) { /* lambda body */ };
        return std::bind(std::move(functional), std::forward<Args>(args)...);
    }
    
    0 讨论(0)
  • 2020-12-12 21:10

    The perfect forwarding is another question, I'm curious is it possible here at all?

    Well... it seems to me that the perfect forwarding is the question.

    The capture of ts... works well and if you change, in the inner lambda,

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

    with

    printer(ts...);
    

    the program compile.

    The problem is that capturing ts... by value (using [=]) they become const values and printer() (that is a lambda that receive auto&&...vars) receive references (& or &&).

    You can see the same problem with the following functions

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

    From clang++ I get

    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 &&)
         ^
    

    Another way to solve your problem is capture the ts... as references (so [&]) instead as values.

    0 讨论(0)
提交回复
热议问题