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

Inheritance support #13

Closed Pennywise007 closed 4 months ago

Pennywise007 commented 9 months ago

As I see you use structured binding for getting a fields list, but I guess it doesn't work with inheritance. For example:

 struct Characteristics {
     rfl::Field<"name", std::string> name;
     rfl::Field<"surname", std::string> surname;
 };
 struct Bart : Characteristics {};

 const Bart bart = {"Bart", "Simpson"};
 auto& [f1, f2] = bart;

Is a correct code and should work with structured binding, but I can't compile it with the write_and_read function call: rfl/internal/to_ptr_tuple.hpp(44): error C3448: the number of identifiers must match the number of array elements or members in a structured binding declaration

At the same time, this code doesn't work with structured binding from the box:

  struct Homer : Characteristics {
      rfl::Field<"work", std::string> company_name;
  };
  const Homer homer = {"Homer", "Simpson", "Station"};
  [[maybe_unused]] auto& [f1, f2, f3] = homer; // here is an error

Do you have any plans to fix it? Inheritane is not the best invention but it is useful.

liuzicheng1987 commented 9 months ago

Unfortunately, there is no easy way to fix this. However, you can use rfl::Flatten instead.

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

So you could rewrite your code as follows:

  struct Homer {
      rfl::Flatten<Characteristics> characteristics;
      rfl::Field<"work", std::string> company_name;
  };
  const Homer homer = {Characteristics{"Homer", "Simpson"}, "Station"};

This would result in the following JSON string and that JSON string could also be read into your struct:

{"name":"Homer","surname":"Simpson","work":"Station"}

The problem is with inheritance and structured bindings is that structured bindings only work if only one of the structs actually contain fields.

Every non-static data member of E must be a direct member of E or the same base class of E, and must be well-formed in the context of the structured binding when named as e.name. E may not have an anonymous union member. The number of identifiers must equal the number of non-static data members. (https://en.cppreference.com/w/cpp/language/structured_binding)

So, in theory, your first example might work, but it kind of defies the point of using inheritance in the context of our library. The second example could not work. You would have to use rfl::Flatten.

I do not think we can support inheritance until there is native support for reflection in C++ and we wouldn't have to use structured bindings as a workaround.

But I will leave the issue open in the event somebody can come up with something. But don't hold your breath.

travnick commented 9 months ago

@Pennywise007 maybe inheritance is useful, but it's not the best invention. Do not inherit an implementation, but do inherit (implement) an interface.

In this case, you will end up with:

  struct Homer{
    Characteristics characteristics;
    rfl::Field<"work", std::string> company_name;
  };
  const Homer homer = {{"Homer", "Simpson"}, "Station"};
  //or 
  const Homer homer = {Characteristics{"Homer", "Simpson"}, "Station"};
  [[maybe_unused]] auto& [f1, f2] = homer;

So you have type safety here, and easiness of restructuring the properties, since they are wrapped with at least {}.

schaumb commented 7 months ago

It can support these aggregate initializable classes (which have empty bases - or have only one non-empty base class). Just use ubiq-base aggregate-hack to recognize inheritance, see here.

hlewin commented 4 months ago

@liuzicheng1987 Would it be possible to (better) support inheritance in cases where structured bindings are already possible? Consider this example:

  struct S {
    int x;
  };

  struct T : S {};

  T test { 2 };
  auto [ xx ] = test;
  for (const auto& fld : rfl::fields<T>()) {
    std::cout<<fld.name()<<std::endl;
  }

Here, the (somewhat unexpected) output is S::x as the only member ends up being disambuigated by prefixing it with S::. The use-case for those constructs is to have a aggregate-initializable base-class (which hence cannot supply advanced constructors) and supply constructors in a derived class (without data-members).

hlewin commented 4 months ago

Here, the (somewhat unexpected) output is S::x

I see this only happens with clang. gcc produces the expected x. I don't know about other compilers. I am having a look at the source-code.