How To Implement Reflection With an Inline Macro

Reflection is a feature C++ badly needs. Without reflection, everyday tasks such as serialization require a lot of unnecessary boilerplate code.

While a proper language solution may still be years away (see the Reflection TS), we can use macros to at least get part way there. With boost describe, for example, we can write something like this

struct X
{
  int m1;
  int m2;
};

BOOST_DESCRIBE_STRUCT(X, (), (m1, m2))

The out-of-line macro BOOST_DESCRIBE_STRUCT will make information about the members of X available at compile-time.

But out-of-line macros impose a significant maintenance cost. Anytime we make changes to X, we need to also update the out-of-line macro.

In this guide, I’ll show you how to use use the library bbai to reflect a structure using an inline macro so that we can instead write

struct X
{
  BBAI_REFLECT_MEMBER(m1, int);
  BBAI_REFLECT_MEMBER(m2, int);
};

// ...

constexpr size_t num_members = bbai::basrf::num_members_v<X>;
    // num_members is 2

constexpr auto name0 = bbai::basrf::member_name_v<X, 0>;
    // name0 is bast::fixed_string<2>{"m1"}

using type0 = bbai::basrf::member_type_t<X, 0>;
    // type0 is int

X x;
x.m1 = 123;
assert(bbai::basrf::member_get<0>(x) == 123);

By using an inline macro, we don’t need to worry about keeping the structure and macro description in sync.

Here’s how BBAI_REFLECT_MEMBER works:

It expands to a definition and a specialization of the member structure bbai_detail_member_reflection:

#define BBAI_REFLECT_MEMBER(NAME, ...)                                         \
  __VA_ARGS__ NAME;                                                            \
  template <size_t, class> struct bbai_detail_member_reflection;               \
  static constexpr size_t bbai_detail_##NAME##_member_index =                  \
      detail::member_index<struct bbai_detail_##NAME##_tag,                    \
                           bbai_detail_member_reflection>::value;              \
  template <class T>                                                           \
  struct bbai_detail_member_reflection<bbai_detail_##NAME##_member_index, T> { \
    static constexpr bast::fixed_string name = #NAME;                          \
    using type = __VA_ARGS__;                                                  \
    template <class U> static constexpr type U::*offset_v = &U::NAME;          \
  };

The helper detail::member_index counts how many prior specializations exist

namespace detail {
template <size_t, class, template <size_t, class> class>
struct member_index_impl {
  static constexpr size_t value = 0;
};

template <size_t I, class M, template <size_t, class> class T>
  requires requires {
    T<I, M>::name;
  }
struct member_index_impl<I, M, T> {
  static constexpr size_t value = 1 + member_index_impl<I+1, M, T>::value;
};
} // namespace detail

namespace detail {
template <class M, template <size_t, class> class T>
struct member_index {
  static constexpr size_t value = member_index_impl<0, M, T>::value;
};
} // namespace detail

And we can use the specialization to query the name, type, and offset of members by index at compile-time:

template <class T, size_t I>
constexpr auto member_name_v = T::template bbai_detail_member_reflection<
    I, struct bbai_detail_member_tag>::name;

You can see the complete code here.

Let’s see how we might use the reflection macro to write code that serializes json.

We’ll start by writing functions to serialize primitives

template <class T>
requires std::is_integral_v<T>
void print_json(std::ostream& out, T x) noexcept { out << x; }

void print_json(std::ostream& out, std::string_view s) noexcept {
  // Note: for a real implementation, we'd need to escape characters like ",
  // but we're not going to worry about that for this example.
  out << "\"" << s << "\"";
}

Next, we’ll add a function for serializing arrays

template <class T>
void print_json(std::ostream& out, const std::vector<T>& v) noexcept {
  out << "[";
  for (auto& val : v) {
    print_json(out, val);
    if (&val != &v.back()) {
      out << ",";
    }
  }
  out << "]";
}

Then, we’ll add a function to serialize reflected structures

namespace detail {
template <size_t I, size_t N, class T>
void print_json_member(std::ostream& out, const T& x) noexcept {
  constexpr auto name = basrf::member_name_v<T, I>;
  print_json(out, name);
  out << ":";
  print_json(out, basrf::member_get<I>(x));
  if (I < N - 1) {
    out << ",";
  }
}

template <class T, size_t... Indexes>
void print_json_impl(std::ostream& out, const T& x,
                     std::index_sequence<Indexes...>) noexcept {
  constexpr auto N = sizeof...(Indexes);
  out << "{";
  (print_json_member<Indexes, N>(out, x), ...);
  out << "}";
}
}  // namespace detail

template <class T>
requires(std::is_aggregate_v<T>&& basrf::num_members_v<T> >
         0) void print_json(std::ostream& out, const T& x) noexcept {
  detail::print_json_impl(out, x,
                          std::make_index_sequence<basrf::num_members_v<T>>{});
}

Now if we create reflected data types

struct employee {
  BBAI_REFLECT_MEMBER(name, std::string);
  BBAI_REFLECT_MEMBER(salary, int);
};

struct team {
  BBAI_REFLECT_MEMBER(department, std::string);
  BBAI_REFLECT_MEMBER(employees, std::vector<employee>);
};

struct company {
  BBAI_REFLECT_MEMBER(name, std::string);
  BBAI_REFLECT_MEMBER(teams, std::vector<team>);
};

Then we can automatically serialize values without having to write any bespoke code

company acme{
  .name{"Acme"},
  .teams{
      {
          .department{"sales"},
          .employees{
              {
                  .name{"Daffy"},
                  .salary{70'000},
              },
              {
                  .name{"Dot"},
                  .salary{125'000},
              },
          },
      },
      {
          .department{"engineering"},
          .employees{
              {.name{"Wakko"}, .salary{100'000}},
          },
      },
  },
};

print_json(std::cout, company);

outputs

{
   "name":"Acme",
   "teams":[
      {
         "department":"sales",
         "employees":[
            {
               "name":"Daffy",
               "salary":70000
            },
            {
               "name":"Dot",
               "salary":125000
            }
         ]
      },
      {
         "department":"engineering",
         "employees":[
            {
               "name":"Wakko",
               "salary":100000
            }
         ]
      }
   ]
}

You can see the full example here

Stay up to date