refl-cpp relies on the user to properly specify type metadata through the use of the REFL_AUTO
macro.
This macro generated the necessary metadata needed for compile-time reflection to work. The metadata is encoded in the type system via generated type specializations which is why there is currently no other way that using a macro. See example-macro.cpp for what the output of the macro looks like.
NOTE: This is a lot of code, but remember that compilers only validate and produce code for templates once they are used, until then the metadata is just token soup that never reaches code-gen.
There are two supported macro styles. The declarative style is directly transformed into procedural style macros via preprocessor magic. The declarative style is more succint, but generates longer error messages. The declarative style also has a hard-limit of 100 members. This is not something that can be substantially increased due to compiler limits on the number of macro arguments.
refl-cpp exposes access to the metadata through the type_descriptor<T>
type. All of the metadata is stored in static fields on the corresponding specialization of that type, but for convenience, objects of the metadata types are typically used in many places, and can be obtained through calling the trivial constructor or through the reflect<T>
family of functions.
type_descriptor<T>
provides access to the target's name, members and attributes.
name
is of type const_string<N>
which is a refl-cpp-provided type which allows constexpr
string equality, concat and slicing.
In a similar way to type metadata, member metadata is also represented through template specializations. The type_list<Ts...>
is an empty trivial template type, which provides a means for passing that list of types through the type system.
Custom attributes are stored in a constexpr std::tuple
which is exposed through the metadata descriptor.
A
has no members and no attributes defined, members
and attribute
are of type type_list<>
, an empty list of types, and std::tuple<>
, an empty tuple, respectively.Let's use a the following simple Point type definition to demonstrate how field reflection works.
Fields are represented through specializations of the field_descriptor<T, N>
. T
is the target type, and N
is the index of the reflected member, regardless of the type of that member (field or function). field_descriptor<T, N>
is never used directly.
There are multiple ways to get a field's descriptor. The easiest one is by using the name of the member together with the find_one
helper.
Field descriptors provide access to the field's name, type, const-ness, whether the field is static, a (member) pointer to the field and convenience get()
and operator()
methods which return references to that field.
As with type_descriptor<T>
, all of the metadata is exposed through constexpr static fields and functions, but an object is used to access those for convenience purposes and because objects can be passed to functions as values as well.
Point::x
in this example has no custom attributes associated with it, the field.attributes
in the example above will be generated as static constexpr std::tuple<>{}
.We will be using the following type definition for the below examples.
Like fields, functions are represented through specializations of a "descriptor" type, namely, function_descriptor<T, N>
. T
is the target type, and N
is the index of the reflected member, regardless of the type of that member (field or function). function_descriptor<T, N>
is never used directly.
There are multiple ways to get a function's descriptor. The easiest one is by using the name of the member together with the find_one
helper.
Function descriptors expose a number of properties to the user.
Function descriptors can be tricky as they represent a "group" of functions with the same name. Overload resolution is done by the resolve
or invoke
functions of function_descriptor<T, N>
. Only when the function is not overloaded is pointer
available (nullptr
otherwise). A call to resolve
is needed to get a pointer to a specific overload. A call to resolve
is not needed to invoke
the target function. The (*this)
object must be passed as the first argument when a member function is invoked. When invoking a static function, simply provide the arguments as usual.
refl-cpp allows the association of compile-time values with reflectable items. Those are referred to as attributes. There are 3 built-in attributes, which can all be found in the refl::attr
namespace.
property
(usage: function) - used to specify that a function call corresponds to an object property
Built-in support for properties includes:
refl::descriptor::get_property
- returns the property
attributerefl::descriptor::is_property
- checks whether the function is marked with the property
attributerefl::descriptor::get_display_name
- returns the friendly_name
set on the property, if present, otherwise the name of the member itselfbase_types
(usage: type) - used to specify the base types of the target. The bases<Ts...>
template variable can be used in place of base_types<Ts...>{}
Built-in support for base types includes:
refl::descriptor::get_bases
- returns a type_list
of the type descriptors of the base classes (Important: Fails when there is no base_types
attribute)refl::descriptor::has_bases
- checks whether the target type has a base_types
attributedebug<F>
(usage: any) - used to specify a function to be used when constructing the debug representation of an object by refl::runtime::debug
All attributes specify what targets they can be used with. That is done by inheriting from one or more of the marker types found in refl::attr::usage
. These include field
, function
, type
, member
(field
or function
), any
(member
or type
).
Custom attributes can be created by inheriting from one of the usage strategies:
And then used by passing in objects of those types as trailing arguments to the member macros.
The presence of custom attributes can be detected using refl::descriptor::has_attribute<T>
.
Values can be obtained using refl::descriptor::get_attribute<T>
.
NOTE: Most of the descriptor-related functions in refl::descriptor
which take a descriptor parameter can be used without being imported into the current namespace thanks to ADL-lookup (example: get_display_name
is not explictly imported above)
The powerful proxy functionality provided by refl::runtime::proxy<Derived, Target>
in refl-cpp allows the user to transform existing types.
This is a very powerful and extremely low-overhead, but also complicated feature. Delegating calls to invoke_impl
is done at compile-time with no runtime penalty. Arguments are passed using perfect forwarding.
See the examples below for how to build a generic builder pattern and POD wrapper types using proxies.
All utility functions are contained in the refl::util
namespace. Some of the most useful utility functions include:
for_each
- Applies function F to each type in the type_list. F can optionally take an index of type size_t.map_to_tuple(type_list<Ts...>, F&& f)
- Applies function F to each type in the type_list, aggregating the results in a tuple. F can optionally take an index of type size_t.get_instance<T>(std::tuple<Ts...>& ts)
- Returns the value of type U, where U is a template instance of T.NOTE: Most of the utility functions in refl::util
which take a type_list<...>
parameter can be used without being imported into the current namespace thanks to ADL-lookup
Example:
refl-cpp provides a range of type-transforming operations in the refl::trait
namespace. Some of the most commonly used type traits are:
get<N, type_list<Ts...>>
is_container<T>
is_reflectable<T>
is_proxy<T>
as_type_list<T<Ts...>>
contains<T, type_list<Ts...>>
map<Mapper, type_list<Ts...>>
filter<Predicate, type_list<Ts...>>
[trait]_t
and [trait]_v
typedefs and constexpr variables are provided where appropriate.
Utilities incurring runtime penalty are contained in the refl::runtime
namespace. That make it clear when some overhead can be expected.
refl::runtime::invoke
can invoke a member (function or field) on the provided object by taking the name of the member as a const char*
. invoke
compiles into very efficient code, but is not as fast a directly invoking a member due to the needed string comparison. This can be useful when generating bindings for external tools and languages. invoke
filters members by argument and returns types before doing the string comparison, which often reduces the number of comparisons required substantially.
refl-cpp can automatically generate a debug representation for your types based on the type metadata it is provided.
While debug
outputs to a std::ostream
, a std::string
result can also be obtained by debug_str
.