The status of reflection in C++

(16 April 2016)

When the C++ committee met in Jacksonville two months ago, something big happened: the reflection study group, SG7, decided what the basic “language” of reflected C++ should look like. What does that mean? Why do you care? Let me, the co-author of the only “blessed proposal”, explain:

Almost everyone agrees that C++ needs a facility to query C++ code itself: types, functions, data members etc. And that this facility should be a compile time facility, at least as a start. But what should it look like?

Several proposals were on the table over the last few years that SG 7 existed; in Jacksonville those were N4428, P0194 and P0255. Here are the main distinguishing features, and SG7’s recommendation:

How to get reflection data

Two major paths to query an entity (a base-level construct) were proposed: operators or templates. Templates need to obey the one-definition-rule (ODR); any recurrence must be exactly the same as the previous “invocations”. They do not allow to test for “progress” within a translation unit: do we have a definition? Do we have a definition now? And now? For template-based reflection, the answer must always be the same.

But even more importantly, C++ only allows certain kinds of identifiers to be passed as templates arguments. Namespaces, for instance, are not among them. There must be no visible difference between passing a typedef or its underlying type as a template parameter, making it impossible to reflect namespaces or typedefs, or requiring language changes for the sake of reflection.

Operators, on the other hand, are a natural way to extend the language. They do not suffer from any such limitation. Additionally, they signal clearly that the code is reflected, making code review simpler.

Traits versus aggregates

How should reflection data be served? Some proposals were based on structure-like entities. Code could use members on them to drill into the reflection data.

This meant that the compiler needs to generate these types for each access. The objects could be passed around, they would need to have associated storage, at least at compile-time.

The alternative is an extension of the traits system. Here, the compiler needs to generate only data that is actually queried. It was also deemed simpler to extend, once reflection wants to support a more complete feature set, or once reflection wants to cover new language features.

Traits on meta or traits on code?

These traits can be applied on the C++ code itself, as done for the regular C++ type traits, possibly with filters to specify query details. Or, and that is the main distinguishing feature of P0194, an operator can “lift” you onto the meta-level, and reflection traits operate only on that meta level.

P0194

Meta-objects are of a meta-type that describes the available interfaces (meta-functions). All of that can be mapped into regular C++ these days, with some definition of “these days”: meta-objects are types; they are unnamed and cannot be constructed; they are generated by the reflection operator, for instance reflexpr(std::string). Meta-functions are templates that “take” a meta-object and “return” a constexpr value or a different meta-object, for instance get_scope. And the big step for the Jacksonville-revision P0194R0 of the proposal has happened for the meta-types: they are now mapped to C++ concepts! That is obvious, natural and makes the proposal even simpler and even more beautiful.

Reflection-types described by concepts

You can query for instance the type property of a meta-object, using get_type. But not all meta-objects have a type; it would not make sense to call that on the meta-object of a namespace. The meta-object (remember, a type) must be of a certain kind: it must implement the requirements of the meta::Typed concept. The type returned by reflexpr(std) does not satisfy these requirements. Easy. For each meta-type (concept) there exists a test whether a meta-object (that all satisfy the meta::Object concept, by definition) is of that meta-type, i.e. satisfies the concept. For instance, get_type is only valid on those meta-objects for which has_typed_v<meta::Object> is true.

Reflection language versus Reflection library

P0194 proposes the basic ingredients to query reflection in C++. You might find it too basic or too complex. We use it to lay the first few miles of the train track, to agree on the design and specify the “language” used. Once we have that, extending it to become a full C++ reflection library is much simpler than providing a complete feature set and defending the design against ten other proposals in parallel. Matus, the original author, has already shown that P0194 is extensible. Like mad.

And now?

Jacksonville was a big step: SG7 agrees on the recommended design. Now we need to agree on the content. For instance, should reflection distinguish typedefs and their underlying type? Take

struct ArrayRef { using index_type = size_t; using rank_type = size_t; rank_type rank_; };

Should reflection see the type of rank_ being unsigned long or rank_type? The former is how the compiler understands the code (“semantic” reflection), the latter is what the developer wrote (“syntactic” reflection). We are collecting arguments; I know of lots of smart people with convincing arguments for each one of these options.

Matus is currently writing the next revision. He will split the paper: a short one with the wording, and a discussion paper that explains the design decisions of SG7 - a sort of log, collecting the arguments for those who want to know why C++ reflection ends up the way P0194 proposes. The design paper will also contain examples of use cases, for instance a JSON serializer and likely a hash generator. Can you implement you favorite reflection use-case with P0194’s interfaces?

Cheers, Axel.