With C++26 reflection, we can simply iterate all data members of an object:
#include <iostream>
#include <meta>
struct Foo {
int x;
int y;
};
void PrintFoo(const Foo& foo) {
constexpr auto members =
define_static_array(nonstatic_data_members_of(^^Foo, std::meta::access_context::current()));
template for (constexpr auto member : members) {
std::cout << identifier_of(member) << ": " << foo.[:member:] << std::endl;
}
}
int main() {
PrintFoo(Foo { .x = 888, .y = 999 });
return 0;
}
where I can iterate data members with template for.
However, if I want to iterate with STL algorithms such as for_each, transform, ... and even ranges (std::views::xxx) to simplify the codes if the logic gets more complicated, I will get compilation errors, since the STL algorithms uses for instead of template for, and I cannot make the parameter member of the lambda expression be constexpr:
void PrintFoo(const Foo& foo) {
constexpr auto members =
define_static_array(nonstatic_data_members_of(^^Foo, std::meta::access_context::current()));
std::ranges::for_each(members, [&foo](auto member) {
std::cout << identifier_of(member) << ": " << foo.[:member:] << std::endl;
}));
}
<source>:15:59: error: splice operand must be a constant expression
15 | std::cout << identifier_of(member) << ": " << foo.[:member:] << std::endl;
...
<source>:14:27: error: expressions of consteval-only type are only allowed in constant-evaluated contexts
14 | std::ranges::for_each(members, [&foo](auto member) {
| ^~~~~~~
https://gcc.godbolt.org/z/MMsd38Wz7
However, most algorithms are constexpr which means they may be used in const-evaluated contexts. Is there anyway to use STL algorithms here to simplify my code?
There are really two questions here.
Is there anyway to iterate data members with STL algorithms using the C++26 reflection?
This is straightforwardly yes. std::meta::info is just a scalar type, so if you get a std::vector<std::meta::info> from some reflection operation, that is just a regular range, and so all of the standard library algorithms just work. There are many examples in the reflection papers demonstrating this, and indeed it's pretty fundamental to the design of value-based reflection that generic code Just Works.
The simplest (if perhaps quite boring) example is:
constexpr std::array types = {^^int, ^^float, ^^double};
constexpr std::array sizes = []{
std::array<std::size_t, types.size()> r;
std::ranges::transform(types, r.begin(), std::meta::size_of);
return r;
}();
types is a range of reflections, std::meta::size_of is a consteval function, all of this just works.
But the second question is about:
std::ranges::for_each(members, [&foo](auto member) {
std::cout << identifier_of(member) << ": " << foo.[:member:] << std::endl;
}));
Because here, you're not just trying to iterate over the members — you also need them to be constants in the body. identifier_of(member) needs to be a constant expression, and in order for foo.[: member :] to work, member needs to be a constant expression.
This isn't any different from trying to iterate over a vector<int> using ranges::for_each and trying to make differently sized arrays from each int. That doesn't work, because you need the ints to be constant, and they're not. Except maybe it's just more obvious that that doesn't work since we're all very familiar with vector<int> and Reflection is much, much newer.
So situations like this will require template for, or some other mechanism to pass through member as a constant. You can still manipulate the range of reflections with ranges algorithms.
One wacky approach, demonstrating that you can still use ranges algorithms, is to convert each non-static data member into a function pointer that prints it. In order to print it, we need the member as a constant, so something like this:
template <std::meta::info R>
void PrintMember(const Foo& foo) {
std::cout << identifier_of(R) << ": " << foo.[:R:] << std::endl;
}
Note that every specialization of PrintMember has the same signature: void(const Foo&). The template parameter doesn't participate.
That means we can then write this (demo):
void PrintFoo(const Foo& foo) {
constexpr auto fs =
define_static_array(
nonstatic_data_members_of(^^Foo, std::meta::access_context::current())
| std::views::transform([](auto member){
auto f = substitute(^^PrintMember, {std::meta::reflect_constant(member)});
return extract<void(*)(const Foo&)>(f);
})
);
std::ranges::for_each(fs, [&foo](auto f) {
f(foo);
});
}
We're turning every member into a void(*)(const Foo&) — and at that point we can just loop over all of them and call them. We've already stashed away the constant. This also demonstrates that define_static_array can take any range and can be used for any [structural] type too — it's not just for making std::span<std::meta::info const>.