9

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?

6
  • 2
    There's a paper to add this feature open-std.org/JTC1/SC22/WG21/docs/papers/2022/p2564r3.html Not sure what the current status is. Commented Nov 18 at 14:20
  • Although, the "bespoke" consteval solution mentioned in the second table (in Introduction) does work currently. Worth trying that out I guess. Commented Nov 18 at 14:28
  • 2
    Not possible because the thing between [: and :] must be a constant expression, and function parameter names are not constant expressions. It would be possible if you can avoid [:member:] though. Commented Nov 18 at 14:36
  • "Is there anyway to use STL algorithms here to simplify my code?" But isn't "template for" a simpler and more intuitive form? Commented Nov 18 at 14:46
  • @cigien It seems that this proposal has been accepted: github.com/brevzin/cpp_proposals/issues/70 And the example code in the proposal can pass the compilation now: gcc.godbolt.org/z/xjME9x3rf But it may not solve this problem, even in this simple example: std::ranges::for_each(members, [](std::meta::info member) { (void)identifier_of(member); });: gcc.godbolt.org/z/aveE9c31d Commented Nov 18 at 14:53

1 Answer 1

18

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>.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.