Skip to content
Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

Why is a raven like a writing desk?

Thoughts both confusing and enlightening.

Familiar Template Syntax IILEs

elbeno, 23 October, 20196 May, 2024

A lot has already been said in the blogosphere about the use of immediately-invoked lambda expressions (IILEs) for initialization, and they’re certainly very useful.

In C++20, P0428 gives us “familiar template syntax” for lambdas. Now, instead of writing a regular generic lambda:

auto add = [] (auto x, auto y) {
  return x + y;
};

we have the option to use “familiar template syntax” to name the template arguments:

auto add = [] <typename T> (T x, T y) {
  return x + y;
};

This has several uses — see the paper for motivations listed there — but one I want to draw attention to in particular: when we use an FTSL as an IILE, it can simplify code.

Examples, you say? I have two for you.

First, consider the “possible implementation” of std::apply listed on cppreference.com:

namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>)
{
    return std::invoke(std::forward<F>(f), std::get<I>(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>>>{});
}

With an FTSIILE, that can become:

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t)
{
  return [&] <std::size_t... I> (std::index_sequence<I...>) {
      return std::invoke(std::forward(f), 
                         std::get<I>(std::forward(t))...);
  }(std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

With an FTSIILE, we avoid having to forward multiple times: we can capture by reference into the lambda instead, and do the forwarding once, inside. The result for small functions like this is about half as much code, better encapsulated. No need to declare a helper any more just to destructure template arguments.

Here’s another quick example, from Jonathan Boccara’s recent blog post about STL Algorithms on Tuples. In that post, Jonathan presents for_each2, a function template that applies a binary function over two tuples:

template <class Tuple1, class Tuple2, class F, std::size_t... I>
F for_each2_impl(Tuple1&& t1, Tuple2&& t2, F&& f, std::index_sequence<I...>)
{
    return (void)std::initializer_list<int>{(std::forward<F>(f)(std::get<I>(std::forward<Tuple1>(t1)), std::get<I>(std::forward<Tuple2>(t2))),0)...}, f;
}

template <class Tuple1, class Tuple2, class F>
constexpr decltype(auto) for_each2(Tuple1&& t1, Tuple2&& t2, F&& f)
{
    return for_each2_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>{});
}

There is a small problem in the original code here: it’s not right to call std::forward<F>(f) in for_each2_impl and potentially move multiple times from the same callable. But that’s not too important; the function rewritten in C++20 could look like this:

template <class Tuple1, class Tuple2, class F>
constexpr decltype(auto) for_each2(Tuple1&& t1, Tuple2&& t2, F&& f)
{
  return [&] <std::size_t... I> (std::index_sequence<I...>) {
      return (std::invoke(f, std::get<I>(std::forward(t1)),
                          std::get<I>(std::forward(t2))), ...), f;
    }(std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple1>>::value>{});
}

Again, less std::forward boilerplate, better encapsulation, less code overall, and the generated code is identical.

So there you have it. Plain IILEs can improve initialization in regular code with less fuss that it would take to extract a small function, and FTSIILEs can now improve template code in the same way, removing the need for separate functions that previously existed only to destructure template arguments and that necessitated more forwarding boilerplate.

C++

Post navigation

Previous post
Next post

Related Posts

C++ Tuples: the missing functionality

6 April, 201530 June, 2015

C++ provides a strange mix of compile-time and runtime functionality for dealing with tuples. There are some interesting parts, like std::tie to destructure a tuple, and std::tuple_cat to join together several tuples into one. So there is evidence that the standard has been influenced by some functional programming ideas, but…

Read More

Exercising Ranges (part 3)

1 July, 20151 July, 2015

(Start at the beginning of the series if you want more context.) So, I was going to implement monoidal_zip, and to do that, I would clone zip_with.hpp. So I did that. Eric’s a great library author, and the ranges code is pretty easy to read. For the most part I…

Read More

Development of an Algorithm

21 June, 201722 June, 2017

Here’s an exercise: given a nice piece of code sitting in a file, how do you take that code and make it generic, in the style of an STL algorithm? For our example, let’s consider an algorithm that isn’t (yet) in the STL. First, the problem it solves. Imagine that…

Read More
©2026 Why is a raven like a writing desk? | WordPress Theme by SuperbThemes