This paper suggests alternative naming for the current Reflection API1, with the aim to increase consistency and discoverability.
Initiating reflection is currently done via the
reflexpr keyword. It could be argued, this keyword is both inconsistent with previous conventions and somewhat misleading.
We already have multiple keywords, all of which do some sort of reflection:
sizeof- returns size of instances of a type.
alignof- returns required alignment for instances of a type.
offsetof- returns the offset from the beginning of an object.
std::addressof- returns the address of an object.
As you can see, the set of functions is consistent b/w each other, following the what-we-need + “of” naming scheme. There is no reason to not respect this model with the Reflection API as well.
reflexpr “borrows” the “expr” (“expression”) suffix from
constexpr, but uses it with a different meaning:
constexprmeans “Define an expression of type constant [evaluated]”.
reflexprmeans “Reflect this [expression?]”
In the former case “expr” is used to define new type of expression.
In the latter case “expr” does not donate a new type of expression. It actually does not donate anything, beyond “this is a reflection [expression]”, as the argument might not be a C++ expression, strictly speaking, and the end result is just a normal (consteval) expression, not a special, “reflection” one.
Using “expr” not only creates misleading “symmetry”, one that is more confusing then helpful (it is not helpful because it does not hint at anything), but also prevents us from introducing this syntax to actually mean “reflection expression”. For example we could image
reflexpr T == int (“is same”),
reflexpr A : T (“is base of”),
reflexpr A(T) || T(B) (“constructable from”), etc. In other words, we might want to use
reflexpr to do inline reflection, without going through full reflection (meta::info).
At the moment Reflection envisions mirroring type_traits into the its own namespace. This will not remove the need for type_traits, because going back and forth b/w reflection and program code is cumbersome and verbose, probably unavoidably so.
Do not use the “expr” suffix and use “of” instead.
This way we fix both issues, stated above - we break the misleading connection to
constxpr and we increase consistency with the current facilities.
Suggested names are:
reflof (proposed), or alternatively
Another alternative is to use “on”, simply because it matches the verb “to reflect” (on something).
Reification is the reverse of reflection, turning a reflection object into program code. Because one reflection object can represent few different program code entities, multiple keywords are needed to do reification. Here is the complete list, as presented in the Reflection paper:
typename(reflection)A simple-type-specifier corresponding to the type designated by “reflection”. Ill-formed if “reflection” doesn’t designate a type or type alias.
namespace(reflection)A namespace-name corresponding to the namespace designated by “reflection”. Ill-formed if “reflection” doesn’t designate a namespace.
template(reflection)A template-name corresponding to the template designated by “reflection”. Ill-formed if “reflection” doesn’t designate a template.
valueof(reflection)If “reflection” designates a constant expression, this is an expression producing the samevalue (including value category). Otherwise, ill-formed.
exprid(reflection)If “reflection” designates a function, parameter or variable, data member, or an enumerator, this is equivalent to an id-expression referring to the designated entity (without lookup, access control, or overload resolution: the entity is already identified). Otherwise, this is ill-formed.
[: reflection :]OR
unqualid(reflection)If “reflection” designates an alias, a named declared entity, this is an identifier referring to that alias or entity. Otherwise, ill-formed.
[< reflection >]OR
templarg(reflection)Valid only as a template argument. Same as “typename(reflection)” if that is well-formed. Otherwise, same as “template(reflection)” if that is well-formed. Otherwise, same as “valueof(reflection)” if that is well-formed. Otherwise, same as “exprid(reflection)”.
Looking at all these as a whole, it is not hard for one to notice, there is little to no consistency b/w them, meaning:
What is more, there is no “simple” or “smart” option, one which would work in the absence of ambiguity. We could easily imagine scenarios where there is only one possible (or “most correct”) result from a reification, and these scenarios are not really covered.
For example, we could agree, that given
ris some reflection object and
typereturns the type reflection object, there exist expected and unambiguous result from the reification of that object, and that is the concrete type. Right now we must be extra specific, that we want type reification and not something else -
typename(type(r)). This can be seen as redundant.
Besides overall inconsistency and lack of simpler options, some of the operators have issues on their own. I will look at each one separately.
This one is clever, but the fact it reuses a keyword for a different action comes with problems.
First and foremost, parenthesis changing the meaning of a keyword is contra the established practice! Take a look at
sizeof - it can be called with and without parenthesis, depending on the parameter, and it does the same thing.
With and without parenthesis,
typename will do radically different things. We could argue that in both cases, “a type is introduced”, but that does not change the fact these two are radically different, both conceptually and in term of implementation.
Second. By reusing the keyword, we “pretend”, reification of a type is the same as regular name introduction. This is of questionable value. Arguably, we want reification to be glaring obvious in code as it can completely change its meaning - we don’t want to confuse
typename (X::something) with a
Third, the name of the keyword is technically incorrect, as it does not introduce a name - the semantics are much closer to that of
typename. We could even imagine a potential confusion, people expecting
typename(r) to return the name of the type (as a string).
Forth, the confusion with existing keywords will get even worse with metaclasses,2 as
class(something) will be used to introduce them. Considering
class are often interchangeable, people will inevitably confuse
Fifth? Using reification in template code can be hard to read and understand -
typename T::typename(template X<T>::B). (Added a question mark, because I am not sure if the first
typename is needed.) It is worth further noting,
template can do reification as well.
From all reifiers, reusing keywords, this one is least problematic, with least chance of causing confusion. Then again, we might have to write
using namespace namespace(something);, which is not exactly self-explanatory.
typename, overloading this keyword will not do us favors. Arguably here it is even worse, simply because
template is already heavily overloaded:
Class templates, function templates, specializations, disambiguation within templates,
template for, with angle brackets, without brackets, with empty brackets - there is no end! If we add
template with pareths as well, it will be simply too much.
This reifier is fine, on its own, it does what it says. Of course, if one is not aware of reflections it could mean anything.
exprid(reflection) and unqualid(reflection)
These two use the C++ Standard lingo to say “name” - an “id” (“identifier”). This is not user-friendly (more like “expert-friendly”) and contra to the existing usage of the term “id” -
Differentiating b/w these two also requires high-tier knowledge about both Reflection and the C++ language.
This is the only reifier, used in just one specific place and its usage is embedded into its name. This might serve us at the moment, but what if we find other places where its result is useful, some place other then template argument?
[: reflection :] and [< reflection >]
These two create very, very funky looking code.
We are definitely stepping into “looking as another language” territory. What is worse, because the lack of a keyword, reification becomes intertwined with regular code, making it hard to reason about at a glance.
Lack of names also makes these hard to search for on the Internet or in a documentation. One must know and remember the “academic” name, used in the Standard, in order to find information about them.
To have both consistency and discoverability, we introduce a keyword that states the action it performs -
reify will come in two flavors.
reify_*series of keywords for all cases, not handled by the single form.
The single form will be used when it is “obvious” what the intend is, acting as the “simple” or “smart” solution, described above:
The idea is,
reify should work in all places where if it doesn’t, it would feel pedantic or verbose.
For all cases where
reify alone is ambiguous or the user whats a specific result, we will have specialized keywords, like in the current proposal:
reify_t(r), in pace of
reify_ns(r), in pace of
reify_tmp(r), in pace of
reify_v(r), in pace of
reify_nm(r) or the geeky
reify_id(r), in pace of
reify_any(r) as it literally does that, testing any possibility, or the eventually
reify_targ(r), in pace of
exprid(r) we could just use the regular
reify, anticipating this will be the most commonly used operation - to reify “the thing” - the member, the function, the variable, etc. In other words,
reify should try to be as smart and as useful as possible, effectively always returning a something - either unambiguously or defaulting to “the entity”. If this is not feasible, we could think of a concrete reifier like
Realizing the above, the framework becomes consistent not just into itself, but also to a framework, already in the language - the casting ensemble of keywords:
This symmetry b/w casting and reification is not an unwelcome one. Reification can be viewed as a form of casting “from” reflection object “to” program code, and in both cases one object can be cast to multiple different things. Overall, both subsytems will have the same benefits (and arguably similar downsides, like verboseness) - in particular the great discoverability for both humans and tools, making it impossible to confuse what the code does:
Additionally, because we don’t overload keywords, we could use reifiers without parenthesizes:
Notice how close this construct is to “dependent lookup” use of ‘typename’.
Having all reifiers behind similar syntax solves all the problems listed at the beginning of this section:
Currently the library entry points, header and namespace, are called “meta”. This name is unfortunate, because it is too generic.
The term “meta” is already used for:
As you can see, using “meta” to name any one subsystem in C++ will lead to confusion, which of the many metas one has in mind.
Use “reflection” instead. It is simple, direct and unambiguous.
Currently library functions tend to end with "_of", like
type_of(r). This will not play well with any form of alternative calling syntax, one that transforms the call to a member-like syntax -
r @ type_of(). Although such a syntax is not currently in the pipeline, noone knows what will happen in 5,10 or 20 years - we should not close that door prematurely.
This paper does not propose a solution, just makes the remark. A solution will involve revisiting all names and doing some non-mechanical renaming, as some names will clash with existing keywords (
Presented here was a comprehensive renaming of the Reflection API, improving its consistency, both internal and with regard of existing facilities, and discoverability. By adopting such naming schema we shift from heavily intertwined reflection code, reification in particular, to strongly separated, brightly highlighted one. We also have clear naming for both the library as a whole and for individual actions in particular.
Compiletime Metaprogramming: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2019/p1717r0.pdf↩︎