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
901 stars 76 forks source link

Possibility to implement an inspector #144

Closed stakira closed 1 month ago

stakira commented 1 month ago

I'm seeking a way to implement an editor of arbitrary structs using immediate mode GUI. Imagine code like below:

void Inspect<T>(T t) {
  for (const auto& f : rfl::fields<T>()) {
    if (f.is_type<int32_t>()) {
      int32_t value = f.get_as<int32_t>(t);
      int32_t new_value = DrawInt32Editor(value);
      if (new_value != value) {
        f.set_value(t, new_value);
      }
    } else if (f.is_type<std::string>()) {
      ...
    }
  }
}

Currently the get<name|id> or replace<name|id> must be static, which makes the above way to iterate over fields difficult. Is something like this possible to implement in or on top of reflect-cpp?

stakira commented 1 month ago

Was able to get sth to work with

view = rfl::to_view(foo);
view.apply([](const auto& f) {
  if (typeid(*f.value()) == typeid(uint32_t)) {
    ...
  } else if (typeid(*f.value()) == typeid(float)) {
    ...
  }
});

I have a feeling this might be the best way already?

liuzicheng1987 commented 1 month ago

Hi @stakira ,

yes, .apply(...) is meant for exactly these kind of uses cases. The problem with runtime iteration is that you don't have any kind of type safety.

Here is how I would rewrite your example:

#include <type_traits>

const auto view = rfl::to_view(foo);

view.apply([](const auto& f) {
  using Type = typename rfl::remove_cvref_t<decltype(f)>::Type;
  if constexpr (std::is_same<Type, uint32_t*>()) {
    ...
  } else if constexpr (std::is_same<Type, float*>()) {
    ...
  }
});

The beauty about this solution is that you now have compile-time type safety. Once you are inside the branches of the constexpr if, the compiler will know that f.value() is in fact an uint32_t* or float*.

By the way, you can also differentiate by name:


const auto view = rfl::to_view(foo);

view.apply([](const auto& f) {
  if constexpr (f.name() == "f1") {
    ...
  } else if constexpr (f.name() == "f2") {
    ...
  }
});

Question: How would you like to see the documentation improved to make this a bit more clearer?

liuzicheng1987 commented 1 month ago

@stakira, does this resolve the issue? Can we close it?

stakira commented 1 month ago

Thanks!

I realized the apply() is unrolling the tuple using recursive templates, therefore using a regular for loop is not possible.

And the "right way" to deal with this situation seems to be defining a template and its specializations, e.g.:

template <typename T>
void Print(const T& t) {}
void Print(const uint32_t& t) {}
void Print(const float& t) {}

view.apply([&](const auto& f) {
  Print(*f.value());
});

My question is answered. Feel free to close.

Regarding documentation, I think what I'm trying to do is a very common use case. It would be really useful to mention examples we have above in the "Reflective programming" section. To be honest, when you have to put a name or index in the template, like view.get<"age"> or rfl::get<0>, reflection is not all that useful. If you cannot supply names or indexes as variables, you will have to know the struct type beforehand, then it's not that different from just accessing fields in the normal way.

liuzicheng1987 commented 1 month ago

@stakira , all right, thank you for your feedback. I will improve the documentation based on your suggestions, because I think that you are right. This is a common use case.

I think the real problem is that this is documented in rfl::NamedTuple:

https://github.com/getml/reflect-cpp/blob/main/docs/named_tuple.md

But I don't think anyone facing this problem would check the documentation for rfl::NamedTuple and then also understand that rfl::to_view(...) creates the named tuple you need to iterate over your struct.