getml / reflect-cpp

A C++20 library for fast serialization, deserialization and validation using reflection. Supports JSON, BSON, CBOR, flexbuffers, msgpack, TOML, XML, YAML / msgpack.org[C++20]
https://getml.github.io/reflect-cpp/
MIT License
821 stars 66 forks source link

Convenient ways to iterate over fields at compile-time #58

Closed grandseiken closed 5 months ago

grandseiken commented 5 months ago

The library claims to support reflective programming in general, but found that I had to do a bit more work to really make it convenient to iterate over fields of a struct with type information and field names, e.g.

template <typename T, typename F>
void for_each_field(T&& value, F&& f) {
  std::apply(
      [&f]<typename... Fields>(Fields&&... fields) {
        ((f.template operator()<std::remove_cvref_t<Fields>::name_>(
             *std::forward<Fields>(fields)())),
         ...);
      },
      rfl::to_view(value).fields());
}

// example usage
auto print_ints = 
  []<auto FieldName, typename T>(const T& x) {
    if constexpr(std::is_same_v<T, int>) {
      std::cout << FieldName.string_view() << ": " << x << "\n";
    }
  };

struct foo { int x; float y; int z; };
for_each_field(foo{ 1, 2.0f, 3 }, print_ints);

Did I miss something that would have made this easier? Do you think helper functions like this would make sense to include in the library, or maybe in example documentation?

I'm also not sure if I missed a better way to get the name of an rfl::Field as a compile-time value, I had to use the internal name_ member above.

liuzicheng1987 commented 5 months ago

I think a helper function like this would be a good addition, but there are a couple of things I would do differently:

1) I would design it as a method called .apply(...) on the NamedTuple.

https://github.com/getml/reflect-cpp/blob/main/include/rfl/NamedTuple.hpp

(rfl::view(...) just returns a named tuple with pointers to the original fields.)

.apply(...) should also have a const version.

2) I would design it in such a way that the function F expects an actual field. So in your example I would rewrite print_ints as follows:

auto print_ints = 
  [](const auto& field) {
    if constexpr(std::is_same_v<typename Field::Type, *int>) {
      std::cout << field.name() << ": " << *field.value() << "\n";
    }
  };

// Use as follows:
rfl::view(foo{ 1, 2.0f, 3 }).apply(print_ints);

(field.name() will have to be added as well. I already have an implementation, but it is in a feature branch. It works as follows:

struct Field {
   ...
    static std::string name() { return Name().str(); } 
}

https://github.com/getml/reflect-cpp/blob/main/include/rfl/Field.hpp

This works because Field::Name is a Literal and literals have this method:

https://github.com/getml/reflect-cpp/blob/main/include/rfl/Literal.hpp

If you want to do it (you've basically already implemented it), a PR is welcome. If not, I will just do it myself. It's not a lot of work.

liuzicheng1987 commented 5 months ago

@grandseiken , just merged your PR. Thank you so much!