refl-cpp |

Introduction to refl-cpp

# Table of Contents

- Basics
- Metadata definitions
  - Declarative style
  - Procedural style
- Type descriptors
- Field descriptors
- Function descriptors
- Attributes
  - Properties
  - Base types
  - Debug formatter
  - Custom attributes
- Proxies
- Compile-time utilities
  - Functional interaface
  - Metaprogramming
- Runtime utilities
  - Invoking a member by name at runtime
  - Printing debug output

# 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::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` 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.

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

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.

# 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:

- `refl::descriptor::get_property` - returns the `property` attribute
- `refl::descriptor::is_property` - checks whether the function is marked with the `property` attribute
- `refl::descriptor::get_display_name` - returns the `friendly_name` set on the property, if present, otherwise the name of the member itself

## 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:

- `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` attribute

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

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)

# 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:

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

## Metaprogramming

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.

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

using refl::runtime::invoke;
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