A simple, stand-alone, header-only and easily pluggable reflection system.
This provides an API that you can implement for any type so that you can introspect its attributes, methods, parent types and any types it "uses".
The "used types" concept can refer to anything really, but one interesting use case is when registering a type with scripting languages. If you're going to be accessing a type's attributes through a scripting language, chances are you also want to register those attributes' types.
An example use case for this API is the register_type function provided for Lua and Python in my putils library, that inspects a type and registers all its attributes and methods to the scripting language.
A generate_reflection_headers script is provided to automatically generate reflection info. This is however completely optional, and you might prefer writing your reflection info by hand to start with.
Making a type reflectible is done like so:
struct parent {};
struct reflectible : parent {
int i = 0;
int get_value() const { return i; }
};
#define refltype reflectible
putils_reflection_info {
putils_reflection_class_name;
putils_reflection_attributes(
putils_reflection_attribute(i)
);
putils_reflection_methods(
putils_reflection_attribute(get_value)
);
putils_reflection_parents(
putils_reflection_type(parent)
);
putils_reflection_used_types(
putils_reflection_type(int)
);
};
#undef refltype
Note that all the information does not need to be present. For instance, for a type with only two attributes, and for which we don't want to expose the class name:
struct simple {
int i = 0;
double d = 0;
};
#define refltype simple
putils_reflection_info {
putils_reflection_attributes(
putils_reflection_attribute(i),
putils_reflection_attribute(d)
);
};
#undef refltype
Accessing a type's name, attributes, methods and used types is done like so:
int main() {
reflectible obj;
std::cout << putils::reflection::get_class_name<reflectible>() << std::endl;
// Obtaining member pointers
{
putils::reflection::for_each_attribute<reflectible>(
[&](const auto & attr) {
assert(attr.ptr == &reflectible::i);
std::cout << attr.name << ": " << obj.*attr.ptr << std::endl;
}
);
constexpr auto member_ptr = putils::reflection::get_attribute<int, reflectible>("i");
static_assert(member_ptr == &reflectible::i);
}
// Obtaining attributes of a specific object
{
putils::reflection::for_each_attribute(obj,
[&](const auto & attr) {
assert(&attr.member == &obj.i);
std::cout << attr.name << ": " << attr.member << std::endl;
}
);
const auto attr = putils::reflection::get_attribute<int>(obj, "i");
assert(attr == &obj.i);
}
// Obtaining member function pointers
putils::reflection::for_each_method<reflectible>(
[&](const auto & method) {
assert(method.ptr == &reflectible::get_value);
std::cout << method.name << ": " << (obj.*method.ptr)() << std::endl;
}
);
// Obtaining functors to call the method on a given object
putils::reflection::for_each_method(obj,
[](const auto & method) {
// func is a functor that calls obj.get_value()
std::cout << method.name << ": " << method.member() << std::endl;
}
);
putils::reflection::for_each_parent<reflectible>(
[](const auto & type) {
// type: putils::meta::type<parent>
using T = putils_wrapped_type(type.type);
std::cout << typeid(T).name() << std::endl;
}
);
putils::reflection::for_each_used_type<reflectible>(
[](const auto & type) {
// type: putils::meta::type<int>
using T = putils_wrapped_type(type.type);
std::cout << typeid(T).name() << std::endl;
}
);
return 0;
}
All these operations can also be done at compile-time:
constexpr reflectible obj;
constexpr auto & attributes = putils::reflection::get_attributes<reflectible>();
using expected_type = std::tuple<std::pair<const char *, int reflectible:: *>>;
static_assert(std::is_same_v<std::decay_t<decltype(attributes)>, expected_type>);
static_assert(std::get<0>(attributes).second == &reflectible::i);
constexpr auto attr = putils::reflection::get_attribute<int>(obj, "i");
static_assert(attr == &obj.i);
static_assert(*attr == 0);
constexpr size_t count_attributes() {
size_t ret = 0;
putils::reflection::for_each_attribute<reflectible>(
[&](const auto & attr) {
++ret;
}
);
return ret;
}
static_assert(count_attributes() == 1);
Functions and concepts are also provided to check if a type exposes a given property:
static_assert(putils::reflection::has_class_name<reflectible>());
static_assert(putils::reflection::has_attributes<reflectible>());
static_assert(putils::reflection::has_methods<reflectible>());
static_assert(putils::reflection::has_parents<reflectible>());
static_assert(putils::reflection::has_used_types<reflectible>());
static_assert(putils::reflection::with_class_name<reflectible>);
...
Types, attributes and methods can be annotated with custom metadata like so:
struct with_metadata {
int i = 0;
void f() const;
};
#define refltype with_metadata
putils_reflection_info {
putils_reflection_type_metadata(
putils_reflection_metadata("key", "value")
);
putils_reflection_attributes(
putils_reflection_attribute(i, putils_reflection_metadata("meta_key", "meta_value"))
);
putils_reflection_methods(
putils_reflection_attribute(f, putils_reflection_metadata(42, std::string("value")))
);
};
#undef refltype
Metadata are key-value pairs, with no specific constraint regarding the types for either the key or the value.
Metadata can then be accessed directly from the metadata
table in the attribute_info
returned by get_attributes
/get_methods
and iterated on by for_each_attribute
/for_each_method
.
They may also be queried and accessed through helper functions:
template<typename T, typename Key>
constexpr bool has_metadata(Key && key) noexcept;
template<typename Ret, typename T, typename Key>
constexpr const Ret * get_metadata(Key && key) noexcept;
template<typename T, typename Key>
constexpr bool has_attribute_metadata(std::string_view attribute, Key && key) noexcept;
template<typename Ret, typename T, typename Key>
constexpr const Ret * get_attribute_metadata(std::string_view attribute, Key && key) noexcept;
template<typename T, typename Key>
constexpr bool has_method_metadata(std::string_view method, Key && key) noexcept;
template<typename Ret, typename T, typename Key>
constexpr const Ret * get_method_metadata(std::string_view method, Key && key) noexcept;
template<typename ... Metadata, typename Key>
constexpr bool has_metadata(const putils::table<Metadata...> & metadata, Key && key) noexcept;
template<typename Ret, typename ... Metadata, typename Key>
constexpr const Ret * get_metadata(const putils::table<Metadata...> & metadata, Key && key) noexcept;
Making a type reflectible consists in specializing the putils::reflection::type_info
template with (up to) 5 static members that provide type information.
namespace putils::reflection {
template<typename T>
struct type_info {
static constexpr auto class_name = const char *;
static constexpr auto attributes = std::tuple<std::pair<const char *, member_pointer>...>;
static constexpr auto methods = std::tuple<std::pair<const char *, member_pointer>...>;
static constexpr auto parents = std::tuple<putils::meta::type<parent>...>;
static constexpr auto used_types = std::tuple<putils::meta::type<used_type>...>;
};
}
For instance, for the reflectible
struct given as an example above:
template<>
struct putils::reflection::type_info<reflectible> {
static constexpr auto class_name = "reflectible";
static constexpr auto attributes = std::make_tuple(
std::make_pair("i", &reflectible::i)
);
static constexpr auto methods = std::make_tuple(
std::make_pair("get_value", &reflectible::get_value)
);
static constexpr auto parents = std::make_tuple(
putils::meta::type<parent>{}
);
static constexpr auto used_types = std::make_tuple(
putils::meta::type<int>{}
);
};
The type_info
specialization can be easily defined through the use of helper macros, described below.
static constexpr auto class_name = "my_class";
Can be easily generated with putils_reflection_class_name
.
static constexpr auto attributes = std::make_tuple(
std::make_pair("attribute", &MyClass::attribute),
...
);
table mapping strings to pointers to the attributes.
Can be easily generated with putils_reflection_attributes
.
static constexpr auto methods = std::make_tuple(
std::make_pair("method", &MyClass::method),
...
);
table mapping strings to pointers to the methods.
Can be easily generated with putils_reflection_methods
.
static constexpr auto parents = std::make_tuple(
putils::meta::type<parent>{},
...
);
std::tuple
of putils::meta::type
objects for each of the type's parents.
Can be easily generated with putils_reflection_parents
.
static constexpr auto used_types = std::make_tuple(
putils::meta::type<UsedType>{},
...
);
std::tuple
of putils::meta::type
objects for each type used by the class (which should also be registered with scripting systems, for instance).
Can be easily generated with putils_reflection_used_types
.
The following functions are defined to let client code check whether a given type is reflectible.
namespace putils::reflection {
template<typename T>
constexpr bool is_reflectible() noexcept; // Returns true if reflection info, even empty, was provided
template<typename T>
constexpr bool has_class_name() noexcept;
template<typename T>
constexpr bool has_attributes() noexcept;
template<typename T>
constexpr bool has_methods() noexcept;
template<typename T>
constexpr bool has_parents() noexcept;
template<typename T>
constexpr bool has_used_types() noexcept;
}
Once a type is declared reflectible, iterating over any of its reflectible properties is made easy by the following helper functions. Note that calling these functions with a non-reflectible type is supported, and will do nothing.
namespace putils::reflection {
template<typename T, typename Func> // Func: void(const attribute_info & attr)
void for_each_attribute(Func && func) noexcept;
template<typename T, typename Func> // Func: void(const object_attribute_info & attr)
void for_each_attribute(T && obj, Func && func) noexcept;
}
Lets client code iterate over the attributes for a given type.
namespace putils::reflection {
template<typename T, typename Func> // Func: void(const attribute_info & attr)
void for_each_method(Func && func) noexcept;
template<typename T, typename Func> // Func: void(const object_attribute_info & attr)
void for_each_method(T && obj, Func && func) noexcept;
}
Lets client code iterate over the methods for a given type.
namespace putils::reflection {
template<typename T, typename Func> // Func: void(const used_type_info & attr)
void for_each_parent(Func && func) noexcept;
}
Lets client code iterate over the parents for a given type.
namespace putils::reflection {
template<typename T, typename Func> // Func: void(const used_type_info & attr)
void for_each_used_type(Func && func) noexcept;
}
Lets client code iterate over the types used by a given type.
template<typename T, typename Parent>
constexpr bool has_parent();
template<typename T, typename Used>
constexpr bool has_used_type();
template<typename T>
constexpr bool has_attribute(std::string_view name);
template<typename T>
constexpr bool has_method(std::string_view name);
Returns whether T
has the specified parent, used type, attribute or method.
template<typename Member, typename T>
std::optional<Member T::*> get_attribute(std::string_view name) noexcept;
template<typename Member, typename T>
Member * get_attribute(T && obj, std::string_view name) noexcept;
Returns the attribute called name
if there is one.
- The first overload returns an
std::optional
member pointer (orstd::nullopt
) - The second overload returns a pointer to
obj
's attribute (ornullptr
)
template<typename Signature, typename T>
std::optional<Signature T::*> get_method(std::string_view name) noexcept;
template<typename Signature, typename T>
std::optional<Functor> get_method(T && obj, std::string_view name) noexcept;
Returns the method called name
if there is one.
- The first overload returns an
std::optional
member pointer (orstd::nullopt
) - The second overload returns an
std::optional
functor which calls the method onobj
(orstd::nullopt
)
The following macros can be used to greatly simplify defining the putils::reflection::type_info
specialization for a type.
These macros expect a refltype
macro to be defined for the given type:
#define refltype ReflectibleType
...
#undef refltype
Declares a specialization of putils::reflection::type_info
for refltype
.
Declares a specialization of putils::reflection_type_info
for a template type, e.g.:
template<typename T>
struct my_type {};
template<typename T>
#define refltype my_type<T>
putils_reflection_info_template {
...
};
#undef refltype
Used inside a reflectible type to mark the corresponding type_info
as friend
, in order to reflect private fields. Takes as parameter the name of the type.
Defines a class_name
static string with refltype
as its value.
Defines a class_name
static string with the macro parameter as its value.
Defines an attributes
static table of std::pair<const char *, member_pointer>
.
Defines a methods
static table of std::pair<const char *, member_pointer>
.
Defines a parents
static tuple of putils::meta::type
.
Takes the name of an attribute as parameter and generates of pair of parameters under the form "var", &refltype::var
to avoid redundancy when passing parameters to putils::make_table
. For instance:
const auto table = putils::make_table(
"x", &point::x,
"y", &point::y
);
can be refactored to:
#define refltype point
const auto table = putils::make_table(
putils_reflection_attribute(x),
putils_reflection_attribute(y)
);
#undef refltype
Provides the same functionality as putils_reflection_attribute
, but skips the first character of the attribute's name (such as an _
or m
) that would mark a private member. For instance:
const auto table = putils::make_table(
"name", &human::_name,
"age", &human::_age
);
can be refactored to:
#define refltype human
const auto table = putils::make_table(
putils_reflection_attribute_private(_name),
putils_reflection_attribute_private(_age)
);
#undef refltype
Provides the same functionality as putils_reflection_attribute
, but for types. It takes a type name as parameter and expands to putils::meta::type<class_name>{}
to avoid redundancy when passing parameters to putils::make_table
.