Static Visitor Reflection in C++

Swort Elbernux
4 min readMar 14, 2015

--

There have been many attempts to introduce a form of reflection into C++, such as the MOC API from Trolltech’s QT, Boost’s Serialization library, and a variety of metaprogramming and macro tricks. Usually, they’re large, complicated libraries that can interfere with other things you may want to use, such as inheritance, templating, or visibility.

Today, I’m going to talk about one of my favorite C++ techniques for reflection that is easy, fast, and requires no libraries of any kind.

Let’s say we have a struct that we’ll use for keeping track of site metrics, such as hits for English versus French website visitors.

struct hits {
int hits_en;
int hits_fr;
};

We would like to do the usual things, such as JSON marshalling. We might also want to look up a field by name at runtime, or sum up all the fields. We’re going to specifically look at the latter in this post.

The Accept Function

The trick is to expose a function for each class that provides the memory address, type and name for each field in an instance. We’ll call it accept, based on the terminology used by the visitor design pattern.

struct hits {
int hits_en;
int hits_fr;
template <typename This, typename Visitor>
static void accept(This t, Visitor v) {
v->visit(t->hits_en, “hits_en”);
v->visit(t->hits_fr, “hits_fr”);
}
};

Okay, it seems a little on the wordy side, but we’re not using any libraries or macros to hide any complexity. When a visitor calls us, we ‘accept’ the call, ‘visit’ each field in turn, and also provide it with a name. The type argument This either represents a hits* or a const hits*, meaning we can reuse the same code for both read-only and read-write visitors. It’s a simple hack to get around the fact that a member function can’t be templated on const-ness.

It’s worth pointing out that even though this class can now be, say, serialized to JSON, users of the type aren’t paying more in compile times to get that functionality until they actually need it. This doesn’t use any more memory. We’ve just provided a compile-time hook that can be simply ignored most of the time.

Writing a Visitor

Let’s now write a visitor that can sum up any int types it encounters in a supplied object. For all other values, it’ll assume they’re a struct and try to traverse them recursively, summing up their members too. (This means we’ll catch mistakes such as adding doubles later on with a compile error.)

struct summer {
summer() : sum() {}
void visit(int v, const char* name) {
sum += v;
}
template <typename T>
void visit(const T& v, const char* name) {
T::accept(&v, this);
}
int sum;
};

The first visit function is the base case in our recursion, and it is hit whenever we visit an integer field. The templated visit function is the recursive case, which allows us to dig into structs-within-structs. It’s also our entry into the algorithm, since we want to supply an object instance, not a single number.

We got given a name argument, but we didn’t end up using it. Common visitors need it, so I added it here. It’s free unless you refer to it, and the compiler does a great job eliminating unused arguments as simple as const char*, as we’ll see later.

Let’s now write a function to wrap this up for convenience:

template <typename T>
int sum_fields(const T& obj) {
summer s;
s.visit(obj, “”);
return s.sum;
}

We needed to provide a name for the top-level object, so we just gave it an empty string. And, we’re done.

So, is it fast?

Yes. I wrote a test program that creates a stats struct containing hits, then recursively visits the fields in the structure, sums them and returns it as the program’s exit code:

int main() {
stats s;
s.hits.hits_en = 1000;
s.hits.hits_fr = 900;
return sum_fields(s);
}

Let’s see how gcc does with optimizing this tree-walking code:

$ g++ -O9 -S sum.cc
$ cat sum.s
.file “sum.cc”
.section .text.startup,”ax”,@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB8:
.cfi_startproc
movl $1900, %eax
ret
.cfi_endproc
.LFE8:
.size main, .-main
.ident “GCC: (Ubuntu 4.8.2–19ubuntu1) 4.8.2"
.section .note.GNU-stack,””,@progbits

This is the interesting line:

movl $1900, %eax

Basically, the summer’s entire tree-walking was optimized down at compile time to a constant value, and returned by the program. From my experience using this approach for JSON serialization and deserialization, the code generated by the compiler is equally tight.

Some more tricks

Another interesting effect of this technique is that the author of the class is responsible for its serialization, not anyone else. For example, you can synthesize legacy or computed fields for serialization, without polluting the memory layout of your classes, like this:

template <typename T, typename V>
static void visit accept(T t, V v) {
t->visit(hits_en, “hits_en”);
t->visit(hits_fr, “hits_fr”);
int difference = t->hits_en — hits_fr;
t->visit(difference, “hits_en_fr_difference”);
}

If your visitor is mutating the object, you also retain control over how the object’s state is updated, because it’s just a few lines of code, not a framework. Just deserialize into a local variable first, check the values, then apply them if they’re acceptable.

What can you use this for?

Lots of things. Anywhere you’re writing field-by-field boilerplate code in your C++, think whether you can use this technique to eliminate it. And as we’ve seen, you won’t pay for it at runtime.

Have fun!

--

--