c++c++26

How to Use C++26 Reflections Properly and Make the Best Use of Them?


I'm exploring the upcoming C++26 reflections feature and I'm excited about its potential to simplify many tasks that involve introspection and metaprogramming. However, I'm not entirely sure how to use reflections properly and how to leverage them for various use cases effectively.

Could someone provide examples of how to use C++26 reflections correctly? Specifically, I'm interested in:

  1. Basic usage of reflections to introspect type information.
  2. How to iterate over members of a struct or class.
  3. Practical use cases like serialization, debugging, and automated testing.

Any code examples and explanations would be highly appreciated!

Examples of Structs:

struct Person {
    std::string name;
    int age;
    double height;
};

struct Address {
    std::string street;
    int number;
};

Desired Outputs:

  1. Print type information of Person and Address.
  2. Iterate over members of these structs and print their names and types.
  3. Serialize an instance of Person to a string (e.g., JSON format).

Solution

  • Some of the API, particularly around recovering strings that represent names, is still in flux. The latest published revision contains an example of how to build a universal formatter, although we're thinking about introducing an API that is spelled identifier_of(mem) instead.

    So the intended implementation right now (assuming we get expansion statements) would look like this:

    struct universal_formatter {
      constexpr auto parse(auto& ctx) { return ctx.begin(); }
    
      template <typename T>
      auto format(T const& t, auto& ctx) const {
        auto out = std::format_to(ctx.out(), "{}{{", identifier_of(^T));
    
        auto delim = [first=true]() mutable {
          if (!first) {
            *out++ = ',';
            *out++ = ' ';
          }
          first = false;
        };
    
        template for (constexpr auto base : bases_of(^T)) {
          delim();
          out = std::format_to(out, "{}", (typename [: type_of(base) :] const&)(t));
        }
    
        template for (constexpr auto mem : nonstatic_data_members_of(^T)) {
          delim();
          out = std::format_to(out, ".{}=", identifier_of(mem));
    
          // this whole dance because we don't have :?
          ctx.advance_to(out);
          std::formatter<typename [:type_of(mem):]> f;
          std::format_parse_context pctx("");
          (void)f.parse(pctx);
          if constexpr (requires { f.set_debug_format(); }) {
            f.set_debug_format();
          }
          out = f.format(t.[:mem:], ctx);
        }
    
        *out++ = '}';
        return out;
      }
    };
    

    With which, you could:

    struct Person {
        std::string name;
        int age;
        double height;
    };
    
    // explicit opt-in, but note that you don't 
    // have to do anything other than this
    template <> struct std::formatter<Person> : universal_formatter { };
    
    int main() {
        // prints: Person{.name="Caeleb Dressel", .age=27, .height=1.9}
        std::println("{}", Person{.name="Caeleb Dressel", .age=27, .height=1.90});
    }
    

    Since we don't have expansion statements, there's an awkward workaround. But it is still doable: demo.