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) << '
';
}, 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) << '
';
}, 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()
";
}
A(A const&){
std::cout << " A::A(A const&)
";
}
A(A&&){
std::cout << " A::A(A&&)
";
}
~A(){
std::cout << " A::~A()
";
}
};
int main(){
using boost::typeindex::type_id_with_cvr;
A a;
std::cout << "create object end
";
[b = a]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "
";
}();
std::cout << "value capture end
";
[&b = a]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "
";
}();
std::cout << "reference capture end
";
[b = std::move(a)]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "
";
}();
std::cout << "perfect capture end
";
[b = std::move(a)]()mutable{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "
";
}();
std::cout << "perfect capture mutable lambda end
";
capture_call([](auto&& b){
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "
";
}, std::move(a))();
std::cout << "capture_call perfect capture end
";
}
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.