A static for loop in C++
if constexpr, but you want for constexprIn 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 default
operator==. 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.cppand 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 the static_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;
}