57

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"

}
max66
  • 65,235
  • 10
  • 71
  • 111
barney
  • 2,172
  • 1
  • 16
  • 25

3 Answers3

122

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.

Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
  • 2
    I am a bit confused by the C++17 solution (the one that uses std::make_tuple() and std::apply()). First of all, I expect the tuple created by make_tuple() to contain std::decay-ed types, except for arguments that are of type std::reference_wrapper. Moreover, std::apply() will forward the tuple to std::get(), which I believe will return lvalue reference if we passed an lvalue reference to the tuple or an rvalue reference otherwise. So all in all it doesn't look like we have perfectly-forwarded the argument pack in the capture. What am I missing? – Moncef M. May 18 '18 at 15:38
  • @fireboot The captured objects are stored as values inside the lambda object. The 'perfect' capturing refers to the construction of these values which is done by perfect forwarding. The apply will give you always rvalue-references to the captured (in the tuple stored) values, which is equivalent to the C++20 version lambda with mutable defined. I added an example for better understanding. Thanks to you I found a bug in the original capture_call! The previously second make_tuple is now replaced by a second apply which creates a rvalue-references 'tuple-view' to the captured values. – Benjamin Buch May 24 '18 at 19:57
  • 4
    Why make_tuple, not forward_as_tuple? – PiotrNycz Feb 13 '19 at 20:07
  • 1
    To also create copies when references are passed in. – Benjamin Buch Feb 14 '19 at 11:23
  • This was exactly what I needed to pass a variadic lambda that wrapped a C varargs function in the payload of a `std::async` call to parallelise my network access code. – David G Feb 18 '20 at 21:15
  • What does `mutable` stands for in 2nd and 3rd snippets? – kyb Mar 24 '20 at 16:20
  • @kyb `mutable` makes captured variables non-`const`, see https://en.cppreference.com/w/cpp/language/lambda – Benjamin Buch Mar 25 '20 at 17:19
  • `std::make_tuple(std::forward(args) ...)` would be better expressed as `std::forward_as_tuple(args...)` to preserve references – keith Mar 29 '20 at 11:24
  • @keith `make_tuple` doesn't preserve references, it `decay`'s them away. – Benjamin Buch Mar 29 '20 at 12:26
  • @Benjamin Buch, the point is `std::forward_as_tuple(args...)` does not – keith Mar 29 '20 at 18:59
  • 2
    Doesn't the C++20 solution also need the `mutable`? And `Args&& args` should be `Args&& ...args`. – Mr. Wonko Apr 19 '20 at 14:48
  • 1
    @Mr.Wonko You are right about the `...`, I fixed it. Thank you! Whether you need the `mutable` depends on whether the values in the closure should be `const` or not. So yes, if you want the exact behavior of the old workaround, then yes. – Benjamin Buch Apr 20 '20 at 17:08
  • @keith You mean `forward_as_tuple` doesn't preserve references? – Benjamin Buch Apr 20 '20 at 17:17
  • 2
    @Benjamin Buch, I mean the opposite. `std::forward_as_tuple(args...)` preserves references which seems better than decaying them. – keith Apr 20 '20 at 17:18
  • Is the C++20 solution currently supported by a compiler? Which paper proposed that syntax? I tried it using GCC9.3 (most current release) and doesn't seem to work. – Silicomancer Apr 26 '20 at 13:04
  • @Silicomancer GCC 9 and LLVM 9 are working, but you have to use `-std=c++2a`. https://en.cppreference.com/w/cpp/compiler_support (Pack expansion in lambda init-capture) https://wg21.link/P0780R2 – Benjamin Buch Apr 27 '20 at 15:37
  • @BenjaminBuch I tried to compile the C++20 Example using GCC10 with c++2a switch. Getting "error: expected variable name or 'this' in lambda capture list". – Silicomancer Apr 27 '20 at 20:09
  • @Silicomancer Here is a working example with GCC 9.3: https://gcc.godbolt.org/z/epdaNP ; Feel free to post your own code. ;-) – Benjamin Buch Apr 29 '20 at 12:31
  • Well, funny. After restarting my system the example code compiles fine. No idea what went wrong. Thank you very much! – Silicomancer May 01 '20 at 14:53
8

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)...);
}
mfacchinelli
  • 101
  • 2
  • 6
3

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.

max66
  • 65,235
  • 10
  • 71
  • 111