- Basics
- Metadata definitions
- Type descriptors
- Field descriptors
- Function descriptors
- Attributes
- Proxies
- Compile-time utilities
- Runtime utilities
refl-cpp relies on the user to properly specify type metadata through the use of the REFL_AUTO
macro.
struct A {
int foo;
void bar();
void bar(int);
};
REFL_AUTO(
type(A),
field(foo, my::custom_attribute("foofoo")),
func(bar, property(), my::custom_attribute("barbar"))
)
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.
- The metadata should be available before it is first requested, and should ideally be put right after the definition of the target type (forward declarations won't work).
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_AUTO(
type(Fully-Qualified-Type, Attribute...),
field(Member-Name, Attribute...),
func(Member-Name, Attribute...)
)
REFL_AUTO(
template((Template-Parameter-List), (Fully-Qualified-Type), Attribute...),
field(Member-Name, Attribute...),
func(Member-Name, Attribute...)
)
REFL_TYPE(Fully-Qualified-Type, Attribute...)
REFL_FIELD(Member-Name, Attribute...)
REFL_FUNC(Member-Name, Attribute...)
REFL_END
REFL_TEMPLATE((Template-Parameter-List), (Fully-Qualified-Type), Attribute...)
REFL_FIELD(Member-Name, Attribute...)
REFL_FUNC(Member-Name, Attribute...)
REFL_END
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.
// continued from previous example
using refl::reflect;
using refl::descriptors::type_descriptor;
constexpr type_descriptor<A> type{};
constexpr auto type = reflect<A>(); // equivalent
type_descriptor<T>
provides access to the target's name, members and attributes.
type.name; // -> const_string<5>{"Point"}
foo.members; // -> type_list<>{}
foo.attributes; // -> std::tuple<>{}
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.
- Since the type
A
has no members and no attributes defined,members
andattribute
are of typetype_list<>
, an empty list of types, andstd::tuple<>
, an empty tuple, respectively.
Let's use a the following simple Point type definition to demonstrate how field reflection works.
struct Point {
int x;
int y;
};
REFL_AUTO(
type(Point),
field(x),
field(y)
)
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.
constexpr auto type = refl::reflect<Point>();
std::cout << "type " << type.c_str() << ":";
// for_each discovered by Koenig lookup (for_each and decltype(type.members) are in the same namespace)
for_each(type.members, [](auto member) { // template lambda invoked with field_descriptor<Point, 0..1>{}
std::cout << '\t' << member.name << '\n';
});
/* Output:
type Point:
x
y
*/
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.
using refl::reflect;
using refl::util::find_one;
constexpr auto type = reflect<Point>();
constexpr auto field = find_one(type.members, [](auto m) { return m.name == "x"; }); // -> field_descriptor<Point, 0>{...}
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.
// continued from previous example
field.name; // -> const_string<1>{"x"}
field.attributes; // -> std::tuple<>{}
field.is_static; // -> false
field.is_writable; // -> true (non-const)
field.value_type; // -> int
field.pointer; // -> pointer of type int Point::*
Point pt{5, -2};
field.get(pt); // -> int& (5)
field(pt); // -> int& (5)
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.
- Since the field
Point::x
in this example has no custom attributes associated with it, thefield.attributes
in the example above will be generated asstatic constexpr std::tuple<>{}
.
We will be using the following type definition for the below examples.
class Circle {
double r;
public:
Circle(double r) : r(r) {}
double getRadius() const;
double getDiameter() const;
double getArea() const;
};
REFL_AUTO(
type(Circle),
func(getRadius),
func(getDiameter),
func(getArea)
)
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.
using refl::reflect;
using refl::util::find_one;
constexpr auto type = reflect<Circle>();
constexpr auto func = find_one(type.members, [](auto m) { return m.name == "getRadius"; }); // -> function_descriptor<Circle, 0>{...}
Function descriptors expose a number of properties to the user.
// continued from previous example
func.name; // -> const_string<6>{"getRadius"}
func.attributes; // -> std::tuple<>{}
func.is_resolved; // -> true
func.pointer; // -> pointer of type double (Circle::* const)()
using radius_t = double (Circle::* const)();
func.template resolve<radius_t>; // -> pointer of type radius_t on success, nullptr_t on fail.
Circle c(2.0);
func.invoke(c); // -> the result of c.getRadius()
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
RELF_AUTO(
type(Circle),
func(getArea, property("area"))
)
Built-in support for properties includes:
refl::descriptor::get_property
- returns theproperty
attributerefl::descriptor::is_property
- checks whether the function is marked with theproperty
attributerefl::descriptor::get_display_name
- returns thefriendly_name
set on the property, if present, otherwise the name of the member itself
base_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...>{}
REFL_AUTO(
type(Circle, bases<Shape>),
/* ... */
)
Built-in support for base types includes:
refl::descriptor::get_bases
- returns atype_list
of the type descriptors of the base classes (Important: Fails when there is nobase_types
attribute)refl::descriptor::has_bases
- checks whether the target type has abase_types
attribute
debug<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:
struct Serializable : refl::attr::usage::member
{
};
And then used by passing in objects of those types as trailing arguments to the member macros.
REFL_AUTO(
type(Circle),
func(getArea, property("area"), Serializable())
)
The presence of custom attributes can be detected using refl::descriptor::has_attribute<T>
.
using refl::reflect;
using refl::descriptor::has_attribute;
for_each(reflect<Circle>().members, [](auto member) {
if constexpr (has_attribute<Serializable>(member)) {
std::cout << get_display_name(member) << " is serializable\n";
}
});
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.
template <typename T>
struct Echo : refl::runtime::proxy<value_proxy<T>, T>
{
template <typename Member, typename Self, typename... Args>
static constexpr decltype(auto) invoke_impl(Self&& self, Args&&... args)
{
std::cout << "Calling " << get_display_name(Member{}) << "\n";
return Member{}(self, std::forward<Args>(args)...);
}
};
Echo<Circle> c;
double d = c.getRadius(); // -> calls invoke_impl with Member=function_descriptor<Circle, ?>, Self=Circle&, Args=<>
// prints "Calling Circle::getRadius" to stdout
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:
for_each(refl::reflect<Circle>(), [](auto m) {});
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.
using refl::runtime::invoke;
Circle c;
double rad = invoke<double>(c, "getRadius"); // calls c.getRadius(), returns double
refl-cpp can automatically generate a debug representation for your types based on the type metadata it is provided.
REFL_AUTO(
type(Circle),
func(getRadius, property("radius")),
func(getArea, property("area"))
)
using refl::runtime::debug;
Circle c(2.0);
debug(std::cout, c);
/* Output: {
radius = (double)2,
area = (double)19.7392
} */
debug(std::cout, c, /* compact */ true);
/* Output: { radius = 2, area = 19.7392 } */
While debug
outputs to a std::ostream
, a std::string
result can also be obtained by debug_str
.