Fishy Magic

Reflection

· by Jameson · Read in about 7 min · (1428 Words)
Tags: c++, code

Lots of programming languages have reflection as a feature out of the box. C++ is not one of them. That doesn’t mean it can’t be done though. Runtime reflection can be added to the language with a little work. But you could instead add ‘Compile-time reflection’ with a lot of work!

This post will assume you are reasonably familiar with templates, and understand at least the very basics of template meta programming. I have attempted to make clear anything more complex as I go.

This code will use modern C++ features, and a C++17 compiler is recommended. I use Visual Studio 2019 for my work. It has some quirks compared to gcc and clang, but it is quite usable.

A snippet of code showing everything discussed below can be found here or here

Type Traits

Most of the information about a type we want reflection for will be stored in a type trait-like structure. This will allow us to check these traits with template code, or if constexpr branches.

Some parts defer to actual type traits to allow us to partially set reflection info about a type.

Example

First, a little example:

struct namedValue
{
	string name;
	int x;
};
REGISTER_REFLECTION_LAYOUT(namedValue)
	MEMBER(name),
	MEMBER(x)
END_REFLECTION_LAYOUT(namedValue)

template<typename T>
void printInfo(T val)
{
	tuple_for_each(Reflection<T>::layout(),[&](auto&& x) {
		// this is C++17 specific. use std::get<0> and std::get<1> in C++14
		auto [name, ptr] = x;
		cout << name << ": " << val.*ptr << endl;
	});
}

If called with namedValue{"numEggs", 37} this might print out:

name: numEggs
x: 37

tuple_for_each is doing a lot of the heavy lifting here: it applies a functor to each element in a tuple, one by one. Reflection::layout() returns a description of the class to be reflected. It contains a tuple of descriptors of the members of the class. Each member’s descriptor is a tuple, containing the name (as a char const*), and a member pointer to that particular member.

The descriptor for namedValue will have the type:

tuple<
	tuple<char const*, string namedValue::*>,
	tuple<char const*, int namedValue::*>
>

Method

The Reflection class contains most of the data. The class is initially defined like so:

template<typename T>
struct ReflectionLayout
{
	static constexpr auto GetLayout()
	{
		return std::make_tuple(
			std::make_tuple(
				"#Unknown", "Unknown"
			)
		);
	}
};

template<typename T>
struct Reflection
{
	static constexpr bool has_reflection_info = false;
	static constexpr char const* name{ "Unknown" };
	static constexpr auto layout() { return ReflectionLayout<T>::GetLayout(); }
	static constexpr bool is_complex{ false };
};

This means we can check if reflection information exists for a type with if constexpr(Reflection<T>::has_reflection_info)

To specify reflection information for our namedValue type above we might write:

template<>
struct ReflectionLayout<namedValue>
{
	static constexpr auto GetLayout()
	{
		return std::make_tuple(
			std::make_tuple("name", &namedValue::name),
			std::make_tuple("value", &namedValue::value)
		);
	}
}

template<>
struct Reflection<namedValue>
{
	static constexpr bool has_reflection_info = true;
	static constexpr char const* name{ "namedValue" };
	static constexpr auto layout() { return ReflectionLayout<T>::GetLayout(); }
	static constexpr bool is_complex{ true };
};

but that’s long and verbose.

Instead we can create preprocessor macros to do the heavy lifting:

#define REGISTER_REFLECTION_INFO(Class) \
template<> \
struct Reflection<Class> \
{ \
	static constexpr bool has_reflection_info = true; \
	static constexpr char const* name{ #Class }; \
	static constexpr auto layout() { return ReflectionLayout<Class>::GetLayout(); } \
	static constexpr bool is_complex{ true }; \
};

#define REGISTER_REFLECTION_LAYOUT(Class) \
template<> \
struct ReflectionLayout<Class> \
{ \
	using Type = Class; \
	static constexpr auto GetLayout() \
	{ \
		return std::make_tuple(

#define MEMBER(Name) std::make_tuple(#Name, &Type::Name)

#define END_REFLECTION_LAYOUT(Class) \
		); \
	} \
}; \
REGISTER_REFLECTION_INFO(Class)

This allows us to write exactly what was in the example to get the same result. The Reflection specialisation must come after the ReflectionLayout specialisation to ensure the layout is available for the Reflection::layout() method.

Using Reflection Information

Earlier we saw tuple_for_each. This function is critical to traversing the reflection layout data. There are a few different ways to implement this function. The approach I prefer requires C++17, but I will describe how to do it in older standards as well.

The API we want is simple enough: pass in the tuple, and some functor. Ideally the functor shouldn’t have a constrained type, but it needs to be able to take a std::tuple<char const*, Class::T*> where T can be the type of any member of Class

This gives us:

template<typename Callable, typename... Args, template<typename...> typename Tuple>
constexpr void tuple_for_each(Tuple<Args...>&& t, Callable&& f)
{
	// ???
}

What we want to do is something like:

f(std::get<1 .. n>(t))...

but std::get requires a single constant argument. There’s also a slight problem in that the ... operator for expanding parameter packs is not valid everywhere. And just to keep things clean: We want a guaranteed order for the iteration. This is harder than it sounds in older C++ standards!

To use std::get the way we want, the common approach is to use std::index_sequence1. By using an inner function, we can create a sequence to match the arity of the tuple:

template<
	typename Callable,
	typename Tuple,
	size_t... Indexes
>
constexpr void tuple_for_each(Tuple&& t, Callable&& f, std::index_sequence<Indexes...>)
{
	// What we'd like to do is
	f(std::get<Indexes>(t))...
	// but that's not actually allowed!
}

template<
	typename Callable,
	typename... Args,
	template<typename...> typename Tuple,
	typename Is = std::make_index_sequence<sizeof...(Args)>
>
constexpr void tuple_for_each(Tuple<Args...>&& t, Callable&& f)
{
	tuple_for_each(t, f, Is{});
}

The next issue is that the ... operator is only allowed in certain contexts. If can be used in initialiser lists, or argument lists to functions. This is handy for pre-C++17 solutions, but C++17 has a neater solution called fold expressions. Fold expressions allow you to expand a pack using any of the binary operators.

For example, if you had a pack containing numbers, you could do:

float total = (NumberPack + ...);

or:

float productDoubled = (2 * ... * NumberPack);

Importantly, you can use a fold expression with the comma operator. So our function body becomes:

(f(std::get<Indexes>(t)), ...);

This will work as hoped!

Appendix

tuple_for_each in C++14 is a little trickier as you can’t use fold expressions. Instead you have to create a context in which the ... operator is permitted. The simplest such context, which has a guaranteed order evaluation, is an array initialisation.

int _[] = {(f(std::get<Indexes>(t)), 0)...};

The , 0 is needed so that each element evaluates to an integer. Both solutions end up using the much maligned comma operator after all!

So What?

This approach can be used as the basis for (almost) anything you can do with reflection in other languages. I’ve used it to write a serialiser, and a GUI data inspector for a game editor. It’s a lot harder to write these things than the equivalent in C#, but it has the same benefits: set up your data, and it already works.

Why not use Runtime Reflection?

At least in theory, compile time reflection is fast. There’s no runtime type inspection, the compiler will already have ensured that the exact right function is called. The downside is there will be a lot of bloat in the compiled code. Every instantiation of tuple_for_each will be compiled into your final executable.

Runtime reflection involves much simpler code, but needs a lot more of it. You’ll need to create types describing each and every concept you want to support, including the basic types. You could hybridise the two approaches, and to be honest every practical reflection system uses at least a little of both.

The full version of the system I’m using in my own project has a bit of runtime reflection for creating objects using a string type name. This is necessary to allow serialising a vector<Base*> containing pointers to objects of various derived types.

And one small extra reason: We may get compile time reflection as a language feature. It’s been discussed a few times, and the approach I’ve taken is (fairly) similar to the better proposals I’ve seen. If those proposals were to be brought into the language, I could do exactly the same thing with far less code!

Future

My own system has a number of extensions on the above. It has support for enums, and can be queried for more information about a type like its size or a unique id. I’ve no doubt I’ll extend it to contain information about methods as well, and probably my runtime reflection will end up bigger than it currently is. If you want to tinker with the code, the example and all supporting code is collected here for C++17, or here, for the C++14 version.

I’m planning to release the full version, with the code around it (serialiser, gui game editor, etc) eventually, but it’s not quite there yet!

Comments