I've seen a lot of examples for Reflection in C++26 that use a new kind of loop that I've never seen before like
template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
What is template for
? What's the point of this, how do I use it, and how does it differ from a regular range-based for
loop?
template for
is a new kind of loop called an expansion statement introduced in C++26 by P1306.
Unlike all the other C++ loops, where the looping is based on some runtime condition, an expansion statement is a loop that happens entirely at compile time. Rather than running the same code N
times, the compound-statement is stamped out N
times during compile time. That is, each kind of expansion statement (which I'll get into shortly) evaluates as:
S0;
S1;
...
Sn-1;
For a synthesized statement Si
.
This allows the loop initializer to actually have a different type or a different constexpr value on each loop iteration. Because each instantiation is just a different statement.
Expansion statements serve three basic purposes:
break
, unlike those awkward workarounds.There are three kinds of expansion statements
The wording itself is quite easy to read, since we're effectively translating the code from an expansion statement into different code that it evaluates as. Would recommend reading that for more clarity.
The simplest case, for when you want to iterate over a pack:
template <class... Ts>
void print_all(Ts... ts) {
template for (auto t : {ts...}) {
std::println("{}", t);
}
}
Each Si
is initializing the declaration with the i
th expression in the pack and evaluating the statement.
The typical case for reflection uses. A lot of reflection functions return a std::vector<std::meta::info>
that you need to iterate over, but you need each meta::info
in that container to be usable as a constant expression for splicing purposes. The usual enum-to-string example looks like this:
template <typename E>
requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
if (value == [:e:]) {
return std::string(std::meta::identifier_of(e));
}
}
return "<unnamed>";
}
We need e
to be a constexpr
variable in order for [:e:]
to be a valid expression.
The rules here match the rules for a regular range-based for
statement (the same rules for begin-expr
and end-expr
as spelled out in [stmt.ranged], except that:
static constexpr
, andit + 1
has to be valid — not all of the other random access operations)Unlike a regular range-based for
loop, e
can be declared constexpr
.
Each Si
is initializing the declaration with the next element of the range and then evaluating the statement. Except this is done statically (unlike a regular range-based for
statement).
That first requirement — that the range variable is declared static constexpr
— actually makes the above example ill-formed because we still cannot have a constexpr
variable that allocates memory, which a std::vector
does (if it's non-empty).
Another reflection example, which also demonstrates the workaround for non-transient allocation, would be printing all of the non-static data members:
template <class T>
void print_members(T const& v) {
std::print("{{");
bool first = true;
constexpr auto ctx = std::meta::access_context::unchecked();
template for (constexpr auto mem : define_static_array(
nonstatic_data_members_of(^^T, ctx)))
{
if (not first) {
std::print(", ");
}
first = false;
std::print(".{}={}", identifier_of(mem), v.[:mem:]);
}
std::print("}}");
}
Given something like:
struct Point { char x; int y; };
the call print_members(Point{.x='e', .y=4})
would print {.x=e, .y=4}
.
The last case is equivalent to structured bindings. That is, iterating over a tuple:
void f() {
int x = 1;
template for (auto p : std::tuple(x, &x)) {
if constexpr (std::is_pointer_v<decltype(p)>) {
std::println("{}", *p);
}
}
}
Each Si
is initializing the declaration (auto p
) with the next element from the structured bindings declaration and then evaluating the statement. Since p
is a templated entity, we do the kind of check we see above, and evaluate *p
— even though that is valid for only one of the types in our tuple.
This is roughly equivalent to
auto&& [...us] = std::tuple(x, &x); // initialization
auto __f = [&](auto p){
if constexpr (std::is_pointer_v<decltype(p)>) {
std::println("{}", *p);
}
};
(__f(us), ...);
Except that this formulation doesn't support break
or continue
, while expansion statements do with the expected semantics (continue
jumps to the beginning of the next iteration, break
jumps to the end of the last iteration).