c++ lambdas how to capture variadic parameter pack from the upper scope
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.
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.
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)...);
}