A static for loop in C++
if constexpr
, but you want for constexpr
In C++, if constexpr
lets you evaluate a condition at compile time, which can make for tidier (or messier) code than following more functional paradigms of template metaprogramming to achieve the same. An example of its use appears in C++: Forwarding references, overload resolution, and taking back control. Similarly, you may desire a for constexpr
to evaluate a loop at compile time, for tidier (or messier) code, although no such construct exists in the language today.
This post demonstrates how to achieve something similar to the non-existent for constexpr
, as well as considering some alternatives that are made available via the C++ language and standard library already.
A static for loop
We can create a static_for
function template:
template<int I, int N, class F>
constexpr void static_for(F f) {
if constexpr (I < N) {
f.template operator()<I>();
static_for<I + 1,N>(f);
}
}
and use it:
static_for<0,3>([&]<int I>() {
// body of loop, where I is loop index
});
The lambda function acts as the body of the loop, with the static argument I
acting as the loop index. What actually happens is that static_for
is a recursive function that is inlined at compile time to ultimately just call the lambda function three times, with I
set to 0
, 1
and 2
, respectively. The if constexpr
ensures termination of the recursion.
Example: comparing tuples
Consider a set of records stored as std::tuple
objects, and comparing them for equality. There is a default comparison operator==
that could be used for this purpose, but for illustration we can use static_for
to implement our own comparison.
While this is mostly for illustration, it may be useful for edge cases. The motivation for this post emerged from a limitation in Nvidia’s CUDA compiler
nvcc
, which seems to have a few issues on the host-device dichotomy when it comes to the defaultoperator==
. This approach provided a workaround.
Complete code could look something like this:
#include <iostream>
#include <tuple>
template<int I, int N, class F>
constexpr void static_for(F f) {
if constexpr (I < N) {
f.template operator()<I>();
static_for<I + 1,N>(f);
}
}
using record = std::tuple<int,double,std::string>;
bool equal_static_for(const record& x, const record& y) {
bool equal = true;
static_for<0,3>([&]<int I>() {
equal = equal && std::get<I>(x) == std::get<I>(y);
});
return equal;
}
int main() {
record a{14, 94.8, "aaa"};
record b{24, 83.3, "bbb"};
record c{14, 94.8, "aaa"};
std::cerr << equal_static_for(a, b) << ' ' << equal_static_for(a, c) << std::endl;
}
If we put this in a file test.cpp
, we can compile with:
g++ -std=c++20 -o test test.cpp
and run with ./test
to obtain:
0 1
There are limitations to such this approach. It is not, of course, an actual loop, and statements like continue and break cannot be used within the lambda function to skip to the next iteration or exit the loop prematurely. A return
statement within the lambda function will return from the lambda function, not the enclosing function—although that offers an interesting prospect, where return
in this context acts like continue in the context of an actual loop.
Alternatives
It is worth looking at some alternative implementations and considerations.
The first is to understand that simple loops with a static trip count are often unrolled by the compiler anyway, and this may be sufficient in many cases. The following loop, for example, will be unrolled when optimizations are enabled, because the trip count (10
) is known at compile time:
for (int i = 0; i < 10; ++i) {
//
}
However, unlike I
in the example above, i
is not constexpr
in this loop; we could pass I
as a template argument in e.g. std::get<I>()
, but we cannot use i
in the same way.
A second alternative is to use std::integer_sequence
from the STL. One way to use it for an equality comparison is as follows:
bool equal_sequence(const record& x, const record& y) {
return [&]<int... Is>(std::integer_sequence<int,Is...>) {
return ((std::get<Is>(x) == std::get<Is>(y)) && ...);
}(std::make_integer_sequence<int,3>());
}
This uses a fold expression for the &&
conjunction.
Personally, I find the
std::integer_sequence
approach less readable that thestatic_for
approach above, but perhaps there are better ways to use it.
A third alternative is that std::tuple
has a default operator==
anyway, at least when its element types do, as in our example. This permits the usual operator==
to work:
bool equal_default(const record& x, const record& y) {
return x == y;
}
Of course, we may be interested in more elaborate computations than equality comparison, this was just an illustrative example.
Summary
While the C++ language does not provide a for constexpr
statement for a static for loop, we can construct something similar, as demonstrated by static_for
here.
Complete code for further play:
#include <iostream>
#include <tuple>
using record = std::tuple<int,double,std::string>;
template<int I, int N, class F>
constexpr void static_for(F f) {
if constexpr (I < N) {
f.template operator()<I>();
static_for<I + 1,N>(f);
}
}
bool equal_static_for(const record& x, const record& y) {
bool equal = true;
static_for<0,3>([&]<int I>() {
equal = equal && std::get<I>(x) == std::get<I>(y);
});
return equal;
}
bool equal_sequence(const record& x, const record& y) {
return [&]<int... Is>(std::integer_sequence<int,Is...>) {
return ((std::get<Is>(x) == std::get<Is>(y)) && ...);
}(std::make_integer_sequence<int,3>());
}
bool equal_default(const record& x, const record& y) {
return x == y;
}
int main() {
record a{14, 94.8, "aaa"};
record b{24, 83.3, "bbb"};
record c{14, 94.8, "aaa"};
std::cerr << equal_static_for(a, b) << ' ' << equal_static_for(a, c) << std::endl;
std::cerr << equal_sequence(a, b) << ' ' << equal_sequence(a, c) << std::endl;
std::cerr << equal_default(a, b) << ' ' << equal_default(a, c) << std::endl;
}