I recently completed a team project which had me producing a game using a custom engine. Whilst developing the engine, it occurred to me that certain features, namely serialization, deserialization and runtime property editing, potentially become tedious to expand upon as the game starts to get bigger. This is due to the engine having to account for new data that is introduced to the code as newer game mechanics are introduced. So, in my pursuit to reduce technical debt, I began searching for a solution to solve this issue.
During my search for a solution, I found 2 possible approaches for this. The first being registering information about the type and its variables at runtime and calling it using strings. This is known as runtime reflection. One of the popular open source libraries being RTTR. Personally I disliked this implementation as I still had to write additional code for registration to RTTR for it to manage my implementation. There is also the possibility of human error when working with strings and runtime related data.
The second solution I found was using macros to generate code for each class. Rather than using code to register the types/variables and call them at runtime, it uses the information it knows at compile time to generate code for the specified purpose. Deni’s blogpost has a great implementation of this and it is also what I based my own code off of.
Implementation C++20
Here is my approach to a minimalistic macro based static reflection: Github Repo
The main difference from Deni’s original code is the generation of the Names array without the use of boost preprocessing library. This was due to my projects own limitations where by I was not allowed to make use of the boost library. Aside from that, I have added an additional Apply function which helps to apply a function onto all members that are exposed via this macro.
References:
#define PARENS ()
#define EXPAND(...) EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__)))
#define EXPAND4(...) EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__)))
#define EXPAND3(...) EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__)))
#define EXPAND2(...) EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__)))
#define EXPAND1(...)
#define FOR_EACH(macro, ...)
__VA_OPT__(EXPAND(FOR_EACH_HELPER(macro, __VA_ARGS__))
#define FOR_EACH_HELPER(macro, a1, ...)
macro(a1)
__VA_OPT__(, FOR_EACH_AGAIN PARENS (macro, __VA_ARGS__)
#define FOR_EACH_AGAIN()
#define STRINGIFY(ARG) #
#define CLASS_INFO(CLASS_NAME, ...)
CLASS_NAME_DEF(CLASS_NAME)
__VA_OPT__(EXPOSE_MEMBERS(CLASS_NAME, __VA_ARGS__))
static_assert(true, "")
#define CLASS_NAME_DEF(CLASS_NAME)
inline static constexpr auto Name()
{
return #CLASS_NAME;
#define EXPOSE_MEMBERS(CLASS_NAME, MEMBER_COUNT, ...)
inline auto Members()
{
return std::tie(__VA_ARGS__);
}
inline auto Members() const
{
return std::tie(__VA_ARGS__);
}
inline static const auto Names()
{
return std::array<std::string_view, MEMBER_COUNT>{FOR_EACH(STRINGIFY, __VA_ARGS__)};
}
template<typename T>
void Apply(T&& _function)
{
auto members = Members();
std::apply(
[&_function]<typename... ClassTypes>(ClassTypes&... _args)
{
((_function(_args)), ...);
}
, members);
}
In this implementation, making use of tuples and macros, we are able to use a functor or templated lambda function to handle each data type accordingly. Hence inducing a form of static reflection whereby the compiler is able to help us deduce which function to call on each variable listed in the class based on the variable’s type.
The main problem this implementation attempts to solve is the ability to iterate over class/struct member variables and be able to retrieve their respective names. This form of reflection is typically used for serialization, deserialization, property editor or loggers. An example of usage can be found below:
template<typename NamesArray>
struct Print
{
NamesArray m_Array;
int counter{ 0 };
template<typename T>
void operator()(const T& _arg)
{
std::cout << m_Array[counter++] << ": " << _arg << std::endl;
}
};
struct SimplePrint
{
template<typename T>
void operator()(const T& _arg)
{
std::cout << _arg << std::endl;
}
};
class Foo
{
int Boo{10};
float Hoo{5.5f};
int Goo{20};
bool Woo{false};
public:
CLASS_INFO(Foo, 4, Boo, Hoo, Goo, Woo);
};
int main()
{
Foo obj;
obj.Apply(Print{ obj.Names() });
obj.Apply(SimplePrint{});
return 0;
}
Output:
Boo: 10
Hoo: 5.5
Goo: 20
Woo: 0
10
5.5
20
0
In this example, I use 2 different forms of printing to output information about the struct. Just by adding the macro to the class/struct and specifying which member variables are to be included in the tuple, we are able to iterate over its members and print out different results based on our preference.
For more complex functionalities that rely on libraries that do not use templates such as rapidjson or imgui, the implementation might require additional work with template specializations for specific types.
I have seen other versions of static reflection implementation may be wrapping the creating a struct/class macro to help define a struct and its members typically changing the way that variables/structs are defined in the project. Personally I prefer using this implementation as the only additional line to add is the macro itself.
After implementing this method into my game engine, my team was able to generate new mechanics and introduce new components without having to worry about managing the serialization or having to write additional code to allow it to show up in my runtime property editor.
Learning points
As I personally have not made use of many macros to this extent, trying to formulate this implementation based on the references was an interesting experience. I learnt that the by using the .i file to view the expanded source code allows me to see how the macro has been expanded and “debug” my macros accordingly. Also that the use of the __VA_OPT__ operator requires the -Zc or the standard conforming preprocessor to be enabled but it was disabled on my visual studios project by default.
Some tips I have came across while working with macros: to force a macro to end with a semicolon, either wrap it in a do while 0 loop for multiline code replacement (within functions) macros or use a static_assert(true,””). The unwrapping of __VA_ARGS__ is vaguely similar to handling variadic template arguments so the ability to think recursively would help in this regard.
Improvements
In this current implementation, the passing of specific variables to expose is done by choice as in my own usage I have found that I do not wish to serialize or expose all member variables of my class to other users of my program. So one possible improvement would be to add prefixes or have the ability to use multiple versions of this macro in the same class to have the option of different variable groups.
Another improvement would be to use a COUNT macro to count the number of variables in __VA_ARGS__ or use some form of static_assert to ensure that the number of strings in the Names array are equivalent to the number of member variables exported as a tuple. This would reduce the chance of human error when defining the struct.
Lastly, the current implementation does not account for struct versioning and is not very suitable for things such as binary serialization after deployment. Meaning that if the order of the struct changes or how the variables are initialized, it is possible that this would break the application based on how the serialization implementation is done. Currently I have not came up with a solution for this.
The future of reflection
To my current knowledge, there are some experimental reflection related features in C++ such as reflexpr, etc which allows the ability to get member variables from a class or struct. These features may make this current macro based implementation obsolete but it was still an extremely interesting experience to work with this. Although I currently have not explored that aspect of C++ yet, I hope to do so in the near future and possibly write a newer implementation of static reflection. Here is a link to a talk about these features from Cppcon 2019: link.

Leave a comment