Iteration at Compile-Time Using C++20 Lambdas

man

In one of my previous articles, “compile-time repeat & noexcept-correctness”, I looked at the design and implementation of a function repeat(f) that, when called, expands into n calls to f at compile time.

For example:

repeat<4>([]{ std::cout << “hello\n”; });

is equivalent to the following code:

[]{ std::cout << “hello\n”; }([]{ std::cout << “hello\n”; });
[]{ std::cout << “hello\n”; }();
[]{ std::cout << “hello\n”; }();
[]{ std::cout << “hello\n”; }();

This mechanism can be viewed as a limited form of iteration at the compilation stage. When writing generalized code, there is often a need for similar constructs for:

  • enumerating the list of types Ts… at the compilation stage;
  • enumerating the list of values Xs… at the compilation stage;
  • iterating over the range [B, E) during compilation;
  • enumerating the list of types Ts… while preserving the iteration index.

In this paper, we will look at the implementation of these constructs using the C++20 innovation P0428: “Familiar pattern syntax for generalized lambdas” (Louis Dionne).

This feature is available in g++ 8.x.

New template syntax for lambdas

An example of a C++20 generalized lambda that takes a template parameter T and works with std::vector:

auto print_vector = [](const std::vector& v) {
for (const auto& x : v) {
std::cout << x;
}
};

print_vector(std::vector{0, 1, 2, 3, 4, 5});

This code compiles into the following anonymous class:

struct {
template
void operator()(const std::vector& v) const {
for (const auto& x : v) {
std::cout << x;
}
}
};

Iteration over the list of types

Example usage:

For_types([]() {
std::cout << typeid(T).name();
});

Implementation:

template
constexpr void for_types(F&&& f) {
(f.template operator()(), …);
}

Iterate over a list of values

Example usage:

for_values<2, 8, 16>([]() {
std::array a;
something(a);
});

Implementation:

template
constexpr void for_values(F&&& f) {
(f.template operator()(), …);
}

Iterating over a range

Example usage:

for_range<-5, 5>([]() {
std::cout << X << ' ';
});

Output:

-5 -4 -3 -2 -1 0 1 2 3 4

Implementation:

template
constexpr void for_range(F&&& f) {
using t = std::common_type_t;
[&f]<auto.... Xs>(std::integer_sequence<t, Xs...>) {
for_values<(B + Xs)...>(f);
}(std::make_integer_sequence<t, E - B>{});

}

Enumerating a list of types with indices

Example usage:

enumerate_types([]() {
std::cout << I << “: ‘ << typeid(T).name() << ’\n';
});

Output:

0: i
1: f
2: c

Implementation:

template
constexpr void enumerate_types(F&&& f) {
[&f](std::index_sequence) {
(f.template operator()(), …);
}(std::index_sequence_for{});
}

Conclusion

The new template syntax in C++20 makes constructs such as for_types and for_range possible and convenient, making them much easier to use than in C++17. The ability to create and use std::integer_sequence directly inside a lambda simplifies the code and makes it more readable.