constexpr reflexpr Document number: P0953R2 , ISO/IEC JTC1 SC22 WG21

, ISO/IEC JTC1 SC22 WG21 Date: 2019-11-19

Authors: Matúš Chochlík <chochlik@gmail.com>, Axel Naumann <axel@cern.ch>, and David Sankel <dsankel@bloomberg.net>

Audience: SG7 Reflection Contents

Abstract

The reflection TS working draft (N4766) provides facilities for static reflection that are based on the template metaprogramming paradigm. Recently, however, language features have been proposed that would enable a more natural syntax for metaprogramming through use of constexpr facilities (See: P0598, P0633, P0712, and P0784). This paper explores the impact of these language facilities on reflexpr and considers what a natural-syntax-based reflection library would look like.

Changes

P0953R2 reworked the document to use type-erased, by-value objects per SG7 consensus at the 2018 San Diego meeting. The poll to use “type-erased” by-value objects was SF:3, F:7, N:2, A:1, SA:2.

P0953R1 introduces a new section, [Library Design Alternative], which, based on feedback from SG7, presents an approach using type-erased, by-value objects. Second, typename was introduced a disambiguator for unreflexpr to address ambiguity in some contexts. Third, a discussion was added to explain the motivation of using pointers instead of references. Finally, the reflect namespace is suggested for placement of type_traits functions.

Introduction

From TMP-reflexpr to CXP-reflexpr

Template-metaprogramming-based reflexpr (TMP-reflexpr), which is succinctly described in P0578, includes a reflexpr operator that, when applied to C++ syntax, produces a type that encodes “meta” information about that syntax. For example, when reflexpr is applied to a type, the result can be used to query that type’s name and, in the case of a class, list its member variables.

Consider the following implementation of a function dump which outputs the name and public data members of an arbitrary type. Note that iteration of the data members involves use of an auxiliary function and variadic templates.

While the above code can be simplified somewhat by use of a template metaprogramming library such as Boost.MPL, this snippet is fairly representative of the complexity and, for most C++ developers, unfamiliarity of the required constructs.

Louis Dionne’s Boost.Hana library provides an alternative approach whereby values with specially designed types allow one to write code that has much of the appearance of normal C++ code, but actually accomplishes metaprogramming (See P0425). Instead of having to write a separate function to accomplish iteration, one could use hana::for_each from within the dump function.

While this mitigates much of the syntactic complexity of template metaprogramming, the types involved are opaque (they cannot be named) and special library features are required to accomplish tasks such as iteration.

Constexpr-based reflexpr (CXP-reflexpr), the subject of this paper, operates in the Hana-style: instead of reflexpr producing a type, it produces a value that encodes meta information. Unlike Hana-style, however, this value has a non-opaque type and we normally do not need special library features to work with it.

The following snippet illustrates our original example using CXP-reflexpr.

Mix compile-time reflection with runtime values

In the above example, we didn’t have a need to mix a runtime value with compile-time meta-information. While this is doable with TMP-reflexpr, additional language facilities are required to accomplish this with CXP-reflexpr.

Consider a function dumpJson that outputs an arbitrary class or struct as a JSON string. Note the use of two new language constructs: constexpr for , and unreflexpr .

The recursive call warrants some additional explanation

pointerToMember is “meta” information about a pointer to a member. In the above example, it refers to &S::m_s in the first iteration of the loop and &S::m_i in the second iteration.

is “meta” information about a pointer to a member. In the above example, it refers to in the first iteration of the loop and in the second iteration. The unreflexpr operator converts this “meta” information about the pointer to member to the pointer to member itself.

operator converts this “meta” information about the pointer to member to the pointer to member itself. t.* is the syntax for accessing a pointer to member.

is the syntax for accessing a pointer to member. dumpJson is the recursive call.

The first iteration is decomposed like this:

The second iteration is similar:

Supporting language constructs

constexpr for

This construct essentially unrolls a for loop at compile-time, allowing for the body of the loop to manifest different types at different iterations. This was originally proposed by Andrew Sutton in P0589 for Hana-style programming, but the unrolling semantics work for our purposes as well.

While this language construct isn’t strictly necessary for CXP-reflexpr, we think it greatly simplifies code that would otherwise require complex library facilities.

constexpr-time allocators

constexpr-time allocators as described in P0784 (also seen in a more preliminary form in P0597) allow us to make use of std::vector , among other data types, at compile time. While these constructs aren’t strictly necessary (resizable arrays based on fixed buffer sizes has been demonstrated by Ben Deane and Jason Turner in P0810), they make the data structures used at compile time more uniform with those at runtime.

unreflexpr

With TMP-reflexpr, types are easily extracted because we are already in a type context.

With CXP-reflexpr, on the other hand, once we have a Type object, there isn’t a sufficient language facility for going back into type processing

One might think decltype would work, but the Type returned by reflexpr(X) is generic, and has no specialized member types for X ; there is nothing X -specific to decltype() on.

Therefore, the one language-level feature that is critical for feature parity with TMP-reflexpr is unreflexpr support. It is required for both types and compile-time values.

Library considerations

A constexpr reflexpr facility has several library-level implications and design questions. These are addressed in this section.

Typeful reflection

Some of the initial sketches (and recently P1240 by Andrew Sutton, Faisal Vali, and Daveed Vandevoorde) for constexpr-based reflection have the reflexpr operation produce values that are always of the same type. While this has some advantages for implementers and is certainly simpler that each value having its own unique type, we feel that making some use of types will encourage programming that is easier to read and reason about.

We also considered using std::variant or variant-like classes as an alternative to reference semantics. Due to the complexity of visitation, the resulting code ended up complex looking, especially for those newer to the language. If a language-level variant with pattern matching support were to be incorporated into C++, then this might be worth revisiting.

References vs. pointers

We were initially tempted to use references to provide a value-semantic experience. Unfortunately, the inability to put a reference directly in a std::vector hinders usability. This design choice would require use of the often confusing std::reference_wrapper template in several places.

Downcasting

While reflecting syntax will produce the most-specific type available, the need to go from a general type to a specific type remains. For example, the return type of Record::get_public_member_types is std::vector<Type> . A user may want to “downcast” one of these types into a Class , for example.

For this, we provide two cast-related operations in our Object class:

These functions allow a user to check if the object can be downcast and to actually perform the operation.

type_traits

The C++ standard library includes several metafunctions that aid in metaprogramming in the <type_traits> header. std::add_const_t is one such example. While these facilities can be used in the CXP-reflexpr paradigm, it is awkward:

We propose that each of these existing metaprogramming functions get a CXP-reflexpr-styled equivalent in the reflect namespace.

Type-erased, by-value objects vs. pointers

While the use of pointers and an inheritance hierarchy for user syntax has benefits, there are two principle problems with this approach. First, as hinted at in P0993r0, the storage and linkage of the pointed-to value raises some implementability concerns that, while likely possible to mitigate, significantly increase the complexity of the approach. Second, we have observed a preference from the committee to use value semantics instead of pointer semantics whenever possible.

Type-erased, by-value objects vs. monotype

P0993r0 advocated for an approach where reflexpr would always return values of type meta::object . This forces use of concepts in the case of overloading. The following snippet shows the changed signatures of outputMetaInformation as described in Typeful reflection:

Note that reflect::Union and reflect::Class are concepts and not types. u and c , in this approach, both have type reflect::object .

There are three principle drawbacks of this approach. First overloading must always use values passed by a template parameter, even when it would not otherwise be necessary. This may substantially discourage creation and maintenance of reflection code by the large number of C++ developers that would not consider themselves experts in the language. Second, requiring concepts to do basic things goes against the desire to make metaprogramming look just like normal programming. Third, the lack of built-in types may result in the proliferation of programs which use reflect::object without concepts and create a maintenance burden.

Type-erased, by-value objects are preferred. This is like the ‘pointer’ approach in that there is a hierarchy of types, but conversion operators are used instead of casting to base classes. For example, the RecordMember type would be,

. This approach provides the benefits of typeful programming and those of the monotype approach.

Consider the difference between the get_public_data_members function of Record with the monotype style,

, and the type-erased, by-value style,

. The latter appears at a glance to be typical C++ code.

Datatypes and Operations

CXP-reflexpr provides a rich type hierarchy representing the various attributes that can be reflected. The following diagram illustrates this hierarchy.

Arrows go from logically derived classes to logically base classes. The blue classes are those that reflexpr directly produces values of. The green classes are those that are intermediate or indirectly available from the other classes.

Classes

What follows is a short synopsis of the hierarchy described above.

reflexpr

reflexpr( texp ) returns values of types according to the first rule that applies:

If texp is a type alias, a TypeAlias is returned.

is a type alias, a is returned. If texp is an enum, a Enum is returned.

is an enum, a is returned. If texp is a class, a Class is returned.

is a class, a is returned. If texp is a union, a Union is returned.

is a union, a is returned. If texp is any other type, a Type is returned.

is any other type, a is returned. If texp is a namespace alias, a NamespaceAlias is returned.

is a namespace alias, a is returned. If texp is a namespace, a Namespace is returned.

is a namespace, a is returned. If texp is an enumerator, a Enumerator is returned.

is an enumerator, a is returned. If texp is a compile-time constant, a Constant is returned.

is a compile-time constant, a is returned. If texp is a variable, a Variable is returned.

unreflexpr

unreflexpr(meta) produces the entities according to the following rules:

If meta has type Type , the result is the type that meta reflects.

has type , the result is the type that reflects. If meta has type Constant , the result is the value that meta reflects. In an otherwise ambiguous context, meta must have type Constant .

unreflexpr typename(meta) produces the entities according to the following rules:

meta must have type Type , the result is the type that meta reflects.

Conclusion

This paper introduces a facility that allows for compile-time reflection using a natural, constexpr-styled programming by taking advantages of new constexpr language features on the horizon. The result is a much simplified interface that makes both reflection and metaprogramming more accessible and maintainable.

Acknowledgments

None of this would be possible without the continued, pioneering work of Daveed Vandevoorde, Louis Dionne, and Andrew Sutton in improving the experience of constexpr programming.