Document #: | |
Date: | |
Project: | Programming Language C++ |
Reply-to: |
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.decltype
.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:
constexpr
means “Define an expression of type constant [evaluated]”.reflexpr
means “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 reflectionof
or reflectof
.
Another alternative is to use “on”, simply because it matches the verb “to reflect” (on something).
reflecton
,reflon
.
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 :]
ORunqualid(reflection)
If “reflection” designates an alias, a named declared entity, this is an identifier referring to that alias or entity. Otherwise, ill-formed.
[< reflection >]
ORtemplarg(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
type(r)
, assumingr
is some reflection object andtype
returns 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.
typename(reflection)
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 typename X::something
.
Third, the name of the keyword is technically incorrect, as it does not introduce a name - the semantics are much closer to that of decltype
, not 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 typename
and class
are often interchangeable, people will inevitably confuse class(property)
with typename(property)
.
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.
namespace(reflection)
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.
template(reflection)
Similar to 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.
valueof(reflection)
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” - typeid
, thread::id
, locale::id
, etc.
Differentiating b/w these two also requires high-tier knowledge about both Reflection and the C++ language.
templarg(reflection)
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
.
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:
int i; constexpr auto r = reflof(i);
reify(type(r)) j; //< int, because of type(r)
float reify('_', r); //< `_i`, because the concatenation overload works on id-s directly
...
for (auto m : members(reflof(C)))
reify(m) = {}; //< member of C, because of members()
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_type(r)
or reify_t(r)
, in pace of typename(r)
.
reify_namespace(r)
or reify_ns(r)
, in pace of namespace(r)
.
reify_template(r)
or reify_tmp(r)
, in pace of template(r)
.
reify_value(r)
or reify_v(r)
, in pace of valueof(r)
.
reify_name(r)
or reify_nm(r)
or the geeky reify_id(r)
, in pace of unqualid(r)
.
reify_any(r)
as it literally does that, testing any possibility, or the eventually reify_targ(r)
, in pace of templarg(r)
.
For 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 reify_entity
or reify_ent
.
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:
static_cast
, dynamic_cast
, const_cast
, reinterpret_cast
.
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:
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 (template_of
).
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.
Reflection: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2019/p1240r1.pdf↩︎
Metaclasses: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0707r4.pdf↩︎
Compiletime Metaprogramming: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2019/p1717r0.pdf↩︎