refl-cpp
Introduction to refl-cpp

Table of Contents

Basics

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).

Metadata definitions

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.

Declarative style

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...)
)

Procedural style

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

Type descriptors

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::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 and attribute are of type type_list<>, an empty list of types, and std::tuple<>, an empty tuple, respectively.

Field descriptors

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.

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, the field.attributes in the example above will be generated as static constexpr std::tuple<>{}.

Function descriptors

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.

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.

Custom Attributes

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.

Properties

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:

Base types

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:

Debug Formatter

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

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>.

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)

Proxies

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.

Compile-time utilities

Functional interface

All utility functions are contained in the refl::util namespace. Some of the most useful utility functions include:

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) {});

Metaprogramming

refl-cpp provides a range of type-transforming operations in the refl::trait namespace. Some of the most commonly used type traits are:

[trait]_t and [trait]_v typedefs and constexpr variables are provided where appropriate.

Runtime utilities

Utilities incurring runtime penalty are contained in the refl::runtime namespace. That make it clear when some overhead can be expected.

Invoking a member by name at runtime

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.

Circle c;
double rad = invoke<double>(c, "getRadius"); // calls c.getRadius(), returns double

Printing debug output

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.

refl::util::find_one
constexpr auto find_one(type_list< Ts... > list, F &&f)
Returns the only instance that matches the constexpr predicate.
Definition: refl.hpp:1797
refl::attr::usage::member
Specifies that an attribute type inheriting from this type can only be used with REFL_FUNC or REFL_FI...
Definition: refl.hpp:1984
refl
The top-level refl-cpp namespace It contains a few core refl-cpp namespaces and directly exposes core...
Definition: refl.hpp:77
refl::runtime::proxy
A proxy object that has a static interface identical to the reflected functions and fields of the tar...
Definition: refl.hpp:3835
refl::reflect
constexpr type_descriptor< T > reflect() noexcept
Returns the type descriptor for the type T.
Definition: refl.hpp:3682
refl::runtime::invoke
U invoke(T &&target, const char *name, Args &&... args)
Invokes the specified member with the provided arguments.
Definition: refl.hpp:4101
refl::descriptor::has_attribute
constexpr bool has_attribute(Descriptor) noexcept
Checks whether T has an attribute of type A.
Definition: refl.hpp:3022
refl::runtime::debug
void debug(std::basic_ostream< CharT > &os, const T &value, bool compact=false)
refl::util::for_each
constexpr void for_each(type_list< Ts... > list, F &&f)
Applies function F to each type in the type_list.
Definition: refl.hpp:1686