Compile-time reflection in C++ 17

refl-cpp 🔥 — constexpr type inspection implemented as a single-header library for C++17 and up.

An example

It was inspired by the C++ Extensions for Reflection proposal and essentially implements a small, but a rather essential subset of functionality, but also build on top of that and makes it available in current compilers. Some examples of features that are made possible by refl-cpp and which are not considered by the Reflection TS are user-defined attributes and user-defined proxy objects.

Furthermore, refl-cpp also allows static iteration and inspection of a type’s member fields, functions, base classes. Static, instance, overloaded and template functions are all supported.

Refl-cpp itself does not directly implement any high-level concepts but instead follows a roll-your-own-implementation mindset, giving you, the developer, the ability to tune it to your needs.

OK, all of that sounds awesome, so how does one do all of that, you may ask? I present you with the simplest usage example.

Before closing this page due to being allergic to macros, here are a few things to keep in mind. First of all, the reflexpr proposal, which is currently scheduled for C++23 (best-case scenario), does not have all features that refl-cpp provides. (Like custom attributes, which are also inspectable at compile-time). Second of all, the user of the library is by no means required to enumerate by hand every single member that a type has. That can be done on a case-by-case basis. Missing type information can also be detected statically, (with static_assert) so that users know that it is needed. Furthermore, the ad-hoc nature of refl-cpp allows type information to be generated for any type, even if it comes from a third-party. (For example, refl-cpp already defines type information for std::string, std::exception, std::tuple). Arbitrary container types also do NOT need type information. All that is needed is for them to define standards-compliant begin() and end() functions. As you can see, refl-cpp does not need as much hand-coding as it seems.

Not back to the example, it might be a lot to take in, so let's break it down. #include “refl.hpp” is all we need to start using refl-cpp. Here we have a definition of a simple POD struct named Point, which has 2 member fields of type float — x and y.

That’s all fine. Then we need to instruct refl-cpp which type we want to be able to use reflection on, as well as which of its members we want to be made available. As you can see, there is some manual typing to be done to enable reflection, but it is quite minimal. One must only specify the name of the type and the names of the reflectable members, and that’s all. No additional type information is needed.

It is in main() where the fun finally begins. We have a variable of type Point named pt. Then we use refl::reflect(pt) to get the type descriptor for Point. (The result of that expression is refl::type_descriptor<Point>{}). Type descriptors have a few members available that allow the user to inspect the target type. One of them is the members property. (Which has a type of refl::util::type_list<…>; type_list is essentially an empty variadic template type, which is used to transport multiple types through the type system). Then we use for_each to iterate all of the members using a lambda function. Here refl-cpp makes use of the fact that lambda functions with arguments declared as auto are actually callables with a templated operator(). All of that means, that the provided lambda can be invoked with different argument types. And that is exactly what happens. For refl-cpp to supports its promise of being a compile-time utility, every member is specified by a different static type. (generally refl::descriptors::field_descriptor<T, N> or refl::descriptors::function_descriptor<T, N>).

An instance of the unique member descriptor type is then passed to the user-provided lambda. All member descriptors share a few common properties, namely name (a compile-time string), attributes (a constexpr tuple) and an operator() overload. The latter is what we use to uniformly invoke a member. (Which for a field_descriptor means taking the value of the reflected field).

And so that is what we do. We print the name of the member through member.name (which is of type refl::util::const_string<N>) and then access the value of the member with the overloaded operator().

Going back to that for_each we used, you may wonder why isn’t it prefix by a namespace. Is it a global function, is it a macro? Thankfully — no. Refl-cpp makes relatively heavy use of some lesser-known (or taken advantage of) C++ features. One of them is relying on Argument-dependent lookup (also known as Koenig lookup). for_each is actually defined in refl::util, but since the type of type_descriptor<T>::members is refl::util::type_list<…>, ADL can find the appropriate implementation without the need for a prefix. It is there along with many utility methods, including map_to_tuple/map_to_array, count_if, etc., all of which are constexpr.

In addition, since all descriptor types are declared in refl::descriptors, we can also use all operations on them unprefixed.

Now, another important feature I mentioned in the beginning but did not show until now is attributes. Without further ado, here is an example.

One of the built-in attributes is the property attribute, which marks a field or function as a special kind of data provider. Properties can take an optional display name, that is available with get_display_name. One can also detect the presence of an attribute using refl::descriptors::has_attribute and get its value directly with refl::descriptors::get_attribute when needed. There are three built-in attributes in refl-cpp, and those are property, debug and bases. Attributes can (and must) specify their usage targets. For example, property can be used with field and function declarations, debug can be used on any declaration, while bases is only available on a type declaration. All built-in attributes are available for use unprefixed.

As previously mentioned, refl-cpp also allows the user to specify custom attributes. Here is an example:

Here we define an attribute called Serializable and specify that it can only be used on member declarations (fields or functions). We then use that attribute on the x and y fields of Point. Upon iteration of the members of Point, we do a compile-time check to see if the member has the needed attribute, then we use its value.

Something, which it would seem some people glance-over, is the *compile-time* aspect of the library. What this means, is that essentially, things, like accessing a field or invoking a function by name, can have *zero-overhead*. Here is how that works: REFL_TYPE(Point) generates a specialization of refl_impl::metadata::type_info__, that refl::descriptors::type_descriptor uses. REFL_FIELD and REFL_FUNC create specializations of the inner type member<N>, defined in each type_info__. refl::descriptors::field_descriptor and refl::descriptors::function_descriptor then wrap these type also. Then, using refl::reflect<Point>(pt) returns refl::type_descriptor<Point>, which has a static constexpr refl::type_list<…> members field. These type lists then support some utility methods, like for_each or filter for example, which are all also constexpr, allowing the user to inspect a type at compile-time and to generate appropriate code, including building a runtime reflection system on top of refl-cpp.

Refl-cpp supports many more features, including custom and automatic debugging (using refl::runtime::debug or refl::runtime::debug_str), reflecting template types (like std::vector or std::pair) and maybe one of the most important features that was not written about here — proxies (aka objects that have an interface identical to that of another object, but can be created automatically). They will be talked about in detail in a follow-up post. Until then, you can see an example of them on the refl-cpp GitHub repo. Also, don’t forget to explore the official refl-cpp documentation.

Please, don’t forget to leave a clap below if found this article interesting and would like to learn more about refl-cpp. Comments are also very welcome. You can also join the discussion on reddit.