August 11, 2018·5 min read·engineeringcppreflectionstandardstemplates

The reflection TS represents program metadata as types

N4766 specifies reflexpr and template operations over reflection metadata. Alias identity and implementation-defined meta-object types explain how this experimental design connects program structure to generic code.

A reflection interface has to decide how ordinary C++ code receives information about its own declarations. The August 11 reflection TS draft, N4766, represents that information through meta-object types. Its reflexpr specifier produces a type, and library templates query properties or transform that type into another reflection handle. The design places compile-time metadata within the machinery of generic programming.

N4766 revises the May draft, N4746. Both are working drafts for experimental extensions. Their facilities should be read through the specified std::experimental::reflect interface and draft syntax, without assuming that ordinary C++17 implementations supply them.

Templates remain one of the parts of C++ I find most useful, despite their compilation and diagnostic costs. Reflection could make generic code depend on declarations the compiler already knows, reducing the need to maintain parallel descriptions manually. The choice to represent metadata as types determines both how that information is consumed and which assumptions library authors must avoid.

The operand describes what is being reflected

N4766 defines reflexpr as a type specifier. Its result is an implementation-defined incomplete class type at namespace scope. That meta-object type supports inspection through the library's traits and transformations. It is a representation of metadata, not the ordinary application type or object named by the operand.

The permitted operands include types, namespaces and enumerators, along with variables and several forms related to functions and expressions. The result satisfies reflection concepts according to the operand's category. Every meta-object type satisfies Object, while more specific categories support additional operations.

That category system matters because reflection does not make every query meaningful for every operand. A namespace has a name and scope relationships, but it is not an enumeration with enumerators. A callable can expose parameter metadata. The concepts express these distinctions so that the operation's requirements can be stated in its template interface.

The library lives in std::experimental::reflect with an inline v1 namespace and the experimental/reflect header. These names are part of this draft's contract. A description that omits the experimental location would make the interface look more settled than its status warrants.

An alias is not identical to the type it names

The draft gives aliases explicit representation. A reflected typedef or using-declaration can carry information about the alias, including the scope into which it was introduced. Transforming that metadata to the aliased entity then answers a different question about the underlying declaration.

A small example illustrates the distinction using the draft interface:

#include <experimental/reflect>

struct record {};
using row = record;

namespace reflect = std::experimental::reflect;
using alias_metadata = reflexpr(row);
using type_metadata = reflect::get_aliased_t<alias_metadata>;

The first metadata type reflects the alias. The transformation obtains metadata for the entity it names. Neither declaration creates a record object. The code supplies type-level handles that subsequent reflection operations can query.

This distinction prevents a naming query from silently answering the wrong question. A program can need the spelling used in an interface, or it can need the underlying type's declaration. Those requirements are different even where both spellings designate the same ordinary C++ type.

The scope operation supplies another example. If an alias in one namespace names a class from another namespace, the alias's scope is the namespace containing that alias declaration. Querying it does not automatically return the namespace containing the original class. The draft's explicit alias category preserves that distinction.

N4766's example with a template type parameter makes the issue concrete. The parameter can reflect both a type and an alias, and the example explicitly applies get_aliased_t before querying the underlying type's name. The transformation is part of the intended interpretation, not an incidental implementation detail.

Metadata identity is not ordinary type identity

The draft leaves it unspecified whether repeated applications of reflexpr to the same operand produce the same type. A library therefore cannot assume that ordinary type identity is a complete test of whether two handles describe the same entity. The reflection operations include reflects_same for that semantic question.

This is a consequence of using implementation-defined types as handles. The compiler needs freedom to represent metadata without promising one globally unique ordinary type for every reflected entity. The library interface must then supply operations that describe the intended relationship independently of that representation choice.

The same distinction appears when translating metadata back into application types. get_reflected_type_t obtains the represented type, while get_type_t applies to a typed entity and yields reflection metadata about its type. Similar-looking transformations can therefore cross different conceptual boundaries. Reading their constraints is necessary to know whether the result is metadata or an ordinary type.

I consider those explicit transformations useful, although they add terminology. Generic code should be clear about whether it is inspecting a declaration or constructing a value of its represented type. Treating both as interchangeable would conceal the operation that actually changes the program's meaning.

Sequences let generic code inspect groups of declarations

N4766 defines object sequences and operations to obtain their size or an element by index. Record operations produce sequences for data members, member functions and base classes. Enumeration operations similarly expose enumerators. This connects compiler-known structure with template operations over a collection of metadata handles.

The record interface distinguishes public, accessible and complete member collections in its operation names. That is significant because a class's full declaration structure is not the same question as which members a particular interface intends to expose. A consumer of reflection metadata needs to choose the collection that matches its purpose.

Other operations expose source location, display names and properties such as whether a callable is deleted or constexpr. The interface consequently goes beyond asking whether an ordinary type has one known trait. It gives generic code a route through relationships among declarations and their associated properties.

The working draft also specifies that obtaining a pointer to a reflected function or static variable odr-uses that entity as though its address were taken. Querying metadata and obtaining an executable reference are therefore distinct operations with different program consequences. A reflection-based library must account for the point where its inspection becomes an actual reference to the reflected program entity.