c++lambdac++14variadic-templatesgeneric-lambda

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


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 lambda as (auto&&...) - should be somehow captured in [=] block.

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

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

}

Solution

  • 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.