Reflection in CPP

It this cursed? Is this totally valid? Only way to know is to use it in prod.


Recently at work, we came across an issue that felt easy, but in CPP is not. We have a collection of classes that need to define a method to serialize themselves to JSON, and we wanted to be sure that all fields in the class were accounted for. Of course, we know better than to trust ourselves, so we were trying to think of automated ways to enforce this property.Unfortunately, our codebase is in C++, and since there’s no mechanism for reflection, it’s simply impossible. In the end, we decided not to enforce it in any way, but it got my mind churning - surely someone else has solved this problem right?

And they have - far better than the method I’m about to detail below, but there’s projects like tsmp which are built for exactly this. tsmp is implemented as a clang tool, which is cool, but I felt like this could be done with my all time favorite C/C++ feature - the preprocessor. So I played around with some sample programs, and it’s actually not that bad!

Before I show my solution to this, you’ll need to understand a relatively common C/C++ preprocessor technique - x lists.

X Lists

X Lists allows one to define some ordered collection of data, and then operate on it multiple times. It’s basically a way of having a list of syntax fragments and then running for-each over it. The trick is to refer to an undefined macro X which becomes a user-suppliable callback.

For example, let’s say I want to build some kind of interactive CLI interface that controls a counter. There’s three commands:

  • quit - closes the program
  • inc - increments the counter
  • get - prints the current counter value

To implement this, I could use an x-list like the following example

#define CommandList \
  X(quit)           \
  X(inc)            \
  X(get)

Now, for each of my commands, I define a callback:

size_t counter = 0;

void handle_quit() {
  exit(0);
}

void handle_inc() {
  counter++;
}

void handle_get() {
  printf("The counter is %zu\n", counter);
}

Finally, we get input from the user:

int main() {
   while (true) {
     auto line = readline();
     // TODO get the appropriate callback
   }
}

Now we could write some if statements comparing every line to each of our commands, but that’s tedious (and error-prone). Instead, we can use our X list!

int main() {
   while (true) {
     auto line = readline();

     if (false) {}
#define X(name) else if (line == #name) { handle_##name(); }
    CommandList;
#undef X
   }
}

Which expands to:

int main() {
   while (true) {
     auto line = readline();

     if (false) {}
     else if (line == "quit") { handle_quit(); }
     else if (line == "inc") { handle_inc(); }
     else if (line == "get") { handle_get(); }
   }
}

What if we also want to introduce a new command - “help” that prints out unique help text for each option? Well, we can go back and modify our X list:

#define CommandList                          \
  X(quit, "closes the program")              \
  X(inc, "increments the counter")           \
  X(get, "prints the current counter value") \
  X(help, "list available commands")

And then our main function, to ignore the descriptions:

...
     if (false) {}
#define X(name, _) else if (line == #name) { handle_##name(); }
...

And all that’s left now is to add our new callback. Since help needs to list all commands, we can use another X macro!

void handle_help() {
  printf("Counter - a simple CLI counter program\n");
  printf("  available commands:\n");
#define X(name, desc) printf("    %s - %s", name, desc);
  CommandList;
#undef X
}

Which will print:

Counter - a simple CLI counter program
  available commands:
    quit - closes the program
    inc - increments the counter
    get - prints the current counter value
    help - list available commands

While this could have been done without X lists, it certainly made things easier. For larger programs where you have many more options, the benefits of this approach becomes more apparent.

Using X Lists to implement Reflection

While thinking about c++’s lack of reflection, I realized that defining types with X lists would give us exactly what we need. We can store the name of a struct field and it’s type, and then retrieve that in other places where we need to iterate over all the members. Let’s look at a small example where we print out the name of every field:

#define DefineField(name, type...) type name;

#define FooFields   \
  X(x, int)         \
  X(y, std::string) \
  X(z, float)

struct Foo {
// Variadic argument for type in case the expression is some complex template
#define X(name, type...) DefineField(name, type);
  FooFields
#undef X
};

Now, to print out every field, we can simply do:

void printFooFields(const Foo& f) {
#define X(name, _) std::cout << #name << " " << f.name << std::endl;
  FooFields;
#undef X
}

This way when ever you update the list of members of Foo, you don’t need to remember to update printFooFields

Using this approach, I’ve made a bigger example where I implement a pretty printer for structs, click here to see it!

Conclusion

This is a viable way to implement reflection in C++, and it’s honestly not even that cursed. However, it does restrict how you define and add member variables to your structs, which I could see becoming messy. I’m not sure I want to introduce this to our codebase at work, but I could see myself doing this for smaller projects and being perfectly happy with it.

Written on February 23, 2023