cpp20_lambda_familiar_template_syntax
C++20 Lambdas: Familiar Template Syntax - Ben Deane
Basics
// C++14: I can only say "auto" here
auto fnc_in_cpp14 = [](auto&& x, auto&& y) {
// I have to recover the types through decltype
something_else(std::forward<decltype(x)>(x), std::forward<decltype(y)>(y));
};
// In C++20, we get "Familiar Template Syntax lambdas".
// C++20: I can name the types now
auto fnc_in_cpp20 = []<class X, class Y>(X&& x, Y&& y) {
// And use the names of type here - no longer need the decltype
something_else(std::forward<X>(x), std::forward<Y>(y));
};
// P0428 Familiar Template Syntax for Generic Lambdas
// A "template head" is now in the syntax,
// to make access to the deduced types much easier.
So where would we use it?
// where would you use it? consider `std::apply`: where I need to delegate to
// another function just to destructure the template argument.
namespace detail {
template <class F, class Tuple, std::size_t... Is>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t,
std::index_sequence<Is...>) {
return std::invoke(std::forward<F>(f),
std::get<Is>(std::forward<Tuple>(t))...);
}
} // namespace detail
template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t) {
return detail::apply_impl(
std::forward<F>(f), std::forward<Tuple>(t),
std::make_index_sequence<
std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}
// C++20: How about immediately-invoking a lambda instead of delegating?
// A familiar template syntax immediately invoked lambda expression(FTSIILE).
template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t) {
return [&]<auto... Is>(std::index_sequence<Is...>) {
return std::invoke(std::forward<F>(f),
std::get<Is>(std::forward<Tuple>(t))...);
}
(std::make_index_sequence<
std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}
Another example: for_each on tuple
for_each on unary function - not too bad
// simple unary for_each on tuples, using std::apply.
template <class Tuple, class F>
F for_each(Tuple&& t, F&& f) {
return std::apply(
[&]<class... Ts>(Ts&&... ts) { (f(std::forward<Ts>(ts)), ...); },
std::forward<Tuple>(t)),
f;
}
// But it gets trickier if we want a binary function over 2 tuples,
// or an n-ary function over multiple tuples.
Not so good when we even with 2-ary function for_each
// C++ 14 way, the same, you have to use a helper function
template <class Tuple1, class Tuple2, class F, std::size_t... Is>
F for_each_impl(Tuple1&& t1, Tuple2&& t2, F&& f, std::index_sequence<Is...>) {
return (std::invoke(f, std::get<Is>(std::forward<Tuple1>(t1)),
std::get<Is>(std::forward<Tuple2>(t2))),
...),
f;
}
template <class Tuple1, class Tuple2, class F>
constexpr decltype(auto) for_each(Tuple1&& t1, Tuple2&& t2, F&& f) {
return for_each_impl(
std::forward<Tuple1>(t1), std::forward<Tuple2>(t2), std::forward<F>(f),
std::make_index_sequence<
std::tuple_size<std::remove_reference_t<Tuple1>>::value>{});
}
// C++ 20 way:
// Use reference-capture and variadic template head.
template <class Tuple1, class Tuple2, class F>
constexpr decltype(auto) for_each(Tuple1&& t1, Tuple2&& t2, F&& f) {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return (std::invoke(f, std::get<Is>(std::forward<Tuple1>(t1)),
std::get<Is>(std::forward<Tuple2>(t2))),
...),
f;
}
(std::make_index_sequence<
std::tuple_size<std::remove_reference_t<Tuple1>>::value>{});
}
// Less forwarding, less noise.
Conclusion A LITTLE C++20 GOODNESS
- If you're delegating to an ancillary function just to destructure a type, consider a Familiar Template Syntax Immediately-Invoked Lambda Expression.
- No more decltype
- Reference-capture provides automatic value category preservation
- Simpler argument handling
- Especially useful for destructuring with
std::index_sequence - FTSIILES FTW!