Skip to content

Custom classes

Reflection implies that all your fields are public. But in object-oriented programming, you often don't want that. If your class is more than a trivial, behaviorless struct, you often want to make your fields private.

If you want your class to be supported by reflect-cpp, it needs to have the following:

1) It needs to publicly define a type called ReflectionType using using or typedef. 2) It needs to have a constructor that accepts your ReflectionType as an argument. 3) It needs to contain a method called reflection that returns said ReflectionType (or a reference thereto).

If you class fulfills these three conditions, then it is fully supported by all serialization and deserialization routines in reflect-cpp.

If you absolutely do not want to make any changes to your original class, you can implement a custom parser.

Example 1: Using an Impl struct

struct PersonImpl {
    rfl::Rename<"firstName", std::string> first_name;
    rfl::Rename<"lastName", std::string> last_name;
    int age;
};

class Person {
    public:
      // 1) Publicly define `ReflectionType`
      using ReflectionType = PersonImpl;

      // 2) Constructor that accepts your `ReflectionType`
      Person(const PersonImpl& _impl): impl(_impl) {}

      ~Person() = default;

      // 3) Method called `reflection` that returns `ReflectionType`
      const ReflectionType& reflection() const { return impl; }

      // ...add some more methods here...

    private:
        PersonImpl impl;
};

Example 2: Matching variables, the safe way

rfl::Field is designed in a way that you have to explicitly initialize every the field (using rfl::default_value, if necessary), otherwise you will get a compile-time error.

A frequent error that happens during serialization/deserialization is that programmers add a field to their class (Person in this example), but forget to update their serialization routine.

The example as shown below will protect you from any such errors, as all fields will have to be explicitly initialized, otherwise you will get a compile-time error. If you add a new field to Person you will have to add it to PersonImpl as well and then explicitly initialize it in the constructor.

Don't worry operator() in rfl::Field is inlined. There won't be any runtime overhead.

struct PersonImpl {
    rfl::Field<"firstName", std::string> first_name;
    rfl::Field<"lastName", std::string> last_name;
    rfl::Field<"age", int> age;
};

class Person {
    public:
      // 1) Publicly define `ReflectionType`
      using ReflectionType = PersonImpl;

      // 2) Constructor that accepts your `ReflectionType`
      // This as the additional benefit that not only the types,
      // but also the names of the fields will be checked at compile time.
      Person(const PersonImpl& _impl): first_name(_impl.first_name),
          last_name(_impl.last_name), age(_impl.age) {}

      ~Person() = default;

      // 3) Method called `reflection` that returns `ReflectionType`
      ReflectionType reflection() const {
          return PersonImpl{
            .first_name = first_name,
            .last_name = last_name,
            .age = age};
      }

      // ...add some more methods here...

    private:
      rfl::Field<"firstName", std::string> first_name;
      rfl::Field<"lastName", std::string> last_name;
      rfl::Field<"age", int> age;
};

Example 3: Matching variables, the unsafe way

If, for any reason, you absolutely cannot change the fields of your class, you have to make sure that all classes are properly initialized or face runtime errors.

struct PersonImpl {
    // ... same as in Example 1 or 2
};

class Person {
    // 1) Publicly define `ReflectionType`
    using ReflectionType = PersonImpl;

    // 2) Constructor that accepts your `ReflectionType`
    Person(const PersonImpl& _impl): first_name(_impl.first_name()),
        last_name(_impl.last_name()), age(_impl.age()) {}

    // ... same as in Example 2

    private:
      std::string first_name;
      std::string last_name;
      int age;
};