A single-include library for reflection in C++ programs
With reflection | Without reflection |
---|---|
#include <string>
#include <ocead/reflection.hpp>
class example : public ocead::reflection {
REFLECTIVE(example)(
PROP(int, 1000, number);
PROP(public:, std::string, 1001, text) = "Gnampf";
double non_reflective_0;
FUNC(public:, void, 2000, add, (int summand)) {
number += summand;
}
FUNC(std::string const &, 2001, get_text, () const) {
return text;
}
void non_reflective_1();
CTOR(example, 3000, ()) = default;
CTOR(example, 3001, (int n, std::string && t))
: number(n),
text(t) { }
DTOR(example, 4000) = default;
);
private:
double non_reflective_2 = 6.;
}; |
#include <string>
class example {
private: int number;
public: std::string text = "Gnampf";
double non_reflective_0;
public: void add(int summand) {
number += summand;
}
public: std::string const & get_text() const {
return text;
}
void non_reflective_1();
example() = default;
example(int n, std::string && t)
: number(n),
text(t) { }
~example() = default;
private:
double non_reflective_2 = 6.;
}; |
- GNU GCC or Clang supporting C++17
- RTTI is not required
(The following examples are based on the example
class from the example above)
With the macros described below, class-wide unique IDs are added to the classes symbols.
With these IDs, the reflective symbols can be accessed dynamically at runtime.
By default the type of these IDs is std::size_t
,
but you can replace it with any type that produces compile time constant expressions.
ℹ I recommend enum classes as a type for class-wide unique IDs.
Each class definition may contain a reflective block. This block is either delimited by the
BEGIN_REFLECTIVE(example);
...
END_REFLECTIVE();
macros, or contained within the
REFLECTIVE(example)(
...
)
macro, here with example
as the reflective classes name. All reflective symbols must appear in these blocks.
ℹ Reflective blocks may also contain non-reflective symbols. The other way round is non possible.
Reflective properties can be declared with the PROP
macro, like so:
PROP(public:, std::string, 1001, text) = "Gnampf";
Here:
public:
is the access specifier for the reflective property (can be omitted)std::string
the type of the reflective property1001
the class-wide unique ID for the reflective symboltext
the name of the reflective property= "Gnammpf"
the optional default initializer for the reflective symbol
Without access specifier the same declaration would look like this:
PROP(std::string, 1001, text) = "Gnampf";
In this case, the reflective property is private
by default.
Reflective functions can be declared with the FUNC
macro, like so:
FUNC(public: void, 2000, add, (int summand)) {
number += summand;
}
Here:
public:
is the access specifier for the reflective function (can be omitted)void
the return type of the reflective function2000
the class-wide unique ID for the reflective symboladd
the name of the reflective function(int summand)
the parameter list of the function and optional const qualifier{ number += summand; }
the optional function body following the declaration
If the access specifier is omitted, it defaults to public
.
Reflective constructors can be declared almost like functions, but with the CTOR
macro instead:
CTOR(public: explicit, example, 3001, (int n, std::string && t))
: number(n),
text(t) { }
Here:
public: explicit
is the access specifier for the reflective constructor and optional explicit specifier (can be omitted)example
the class name3001
the class-wide unique ID for the reflective symbol(int n, std::string && t)
the parameter list of the constructor: number(n), text(t) { }
the optional initializer list and constructor body
If the access specifier is omitted, it defaults to public
.
Reflection destructor can be declared almost like constructors, but with the DTOR
macro instead:
DTOR(public:, example, 4000) = default;
Here:
public:
is the access specifier for the reflective destructor (can be omitted)example
the class name4000
the class-wide unique ID for the reflective symbol= default
the implementation of the destructor
Again, if the access specifier is omitted, it defaults to public
.
Class descriptors provide you with reflective information at runtime.
To obtain a class descriptor of a reflective class, use the ocead::reflect
template function,
like so:
const auto & refl = ocead::reflect<example>();
If you have RTTI enabled for your program, you can additionally obtain a classes class descriptor
by calling the overridden ocead::reflective::reflect
function, like so:
example instance;
const auto & refl = instance.reflect();
These descriptors map the class-wide unique IDs to symbol descriptors that contain information such as name, type and, depending on the kind of symbol, various function pointers.
The difference in calling typed and untyped accessors is where the class-wide unique ID goes:
- For typed accessors the ID becomes a template parameter
auto & number = refl.access<1000>(instance);
- For untyped accessors the ID is a regular parameter
auto & number = *static_cast<int &>(refl.access(&instance, 1000));
For each access
function a const_access
function for accessing const objects exists.
Normally, reflective properties are accesses via generated functions that return the property. For faster access you may use access by offset. Only reflective properties that aren't references can also be accessed by offset from a pointer to an object.
⚠ Accessing members of non standard-layout types is only conditionally supported. Check if your compiler supports offset accessing before using this feature!
The functions for access by offset are offset_access
and const_offset_access
.
They can be called just like their non-offset_
counterparts.
Reflective functions can be invoked with the invoke
functions in class and symbol descriptors.
The call depends on what you know at compile time:
- If you know the ID at compile time:
auto sum = refl.invoke<int, 2000>(&instance, 5);
- If you don't know the ID at compile time:
For the ID-known-at-compile-time variant you only need the
auto sum = refl.invoke_return<int>(&instance, 2000, 5);
invoke function
. For the ID-known-at-run-time variant you have to discern whether to use the invoked functions return value, in which case you have to callinvoke_return
instead, or not. Then it is:refl.invoke(&instance, 2000, 5);
In any case, you pass a pointer to the object on which the reflective function shall be invoked on,
then the optional id, followed by the arguments with which to call the function.
Pass the arguments just as you would when calling std::invoke
.
Feedback and suggestions in regard to resolving the ambiguity between invocations a very welcome.
Constructing reflective objects is very straightforward. You can construct an instance of a reflective class in one of three ways:
- new
This constructs the instance using
void * instance = refl.construct(3001, 6, std::string(""));
new
. - placement new
This constructs the instance using placement
alignas(example) std::byte buffer[sizeof(example)]; void * instance = refl.placement_construct(buffer, 3001, 6, std::string(""));
new
. - shared new
This constructs the instance using
auto instance_ptr = refl.shared_construct(buffer, 3001, 6, std::string(""));
new
and returns a smart pointer to the instance.
As with invocations, if you know the class-wide unique ID at compile time, you may pass is as a template parameter.
Destroying reflective objects is even simpler:
- Calling destructors
This calls the destructor in instance.
refl.destroy(4000, &instance);
- Deallocating instances
This calls the destructor in instance and deallocates the occupied memory. Use it just as you would use
refl.dealloc_destroy(4000, &instance);
delete
.
You can generate arbitrary record classes from reflective classes, that contain only the member fields you need, like so:
auto rec = ocead::make_record<1000, 1001>(instance);
The template parameters are the IDs of the properties you want to include into the record.
You can access the records properties with std::get
:
int number = std::get<1000>(rec);
You may customise this library by overriding different macros.
These macros control the types employed by the library:
Macro | Default type | Description |
---|---|---|
OCEAD_REFLECTION_COUNTER_TYPE |
std::size_t |
Used for globally distinguishing reflective symbols |
OCEAD_REFLECTION_ID_TYPE |
std::size_t |
Used as unique identifier by OCEAD_REFLECTION_CLASS_DESCRIPTOR_TYPE |
OCEAD_REFLECTION_TYPEID_TYPE |
char const * |
Used for differentiating (return) types of reflective symbols |
OCEAD_REFLECTION_NAME_TYPE |
char const * |
Used for recording names of reflective symbols |
OCEAD_REFLECTION_CONTAINER_TEMPLATE |
ocead::reflection::type_identity |
May be used to transform the apparent type of a reflective property into a more complex type |
OCEAD_REFLECTION_ACCESSOR_TYPE |
ocead::accessor_t |
Function type for non-const accessors |
OCEAD_REFLECTION_CONST_ACCESSOR_TYPE |
ocead::const_accessor_t |
Function type for const accessors |
OCEAD_REFLECTION_INVOKER_TYPE |
ocead::invoker_t |
Function type for non-const invokers |
OCEAD_REFLECTION_CONST_INVOKER_TYPE |
ocead::const_invoker_t |
Function type for const invokers |
OCEAD_REFLECTION_SHARED_PTR_TYPE |
std::shared_ptr<void> |
Smart pointer type that can hold deleter functions |
OCEAD_REFLECTION_SYMBOL_DESCRIPTOR_TYPE |
ocead::reflection::symbol_descriptor |
Type used for describing single reflective symbols |
OCEAD_REFLECTION_REFLECTOR_MAP_TYPE |
std::map |
Type used for organizing OCEAD_REFLECTION_SYMBOL_DESCRIPTOR_TYPE |
OCEAD_REFLECTION_CLASS_DESCRIPTOR_TYPE |
ocead::reflection::class_descriptor |
Type used for describing reflective classes |
These macros control how other macros in this library are expanded:
Macro | Default expansion | Description |
---|---|---|
OCEAD_REFLECTION_IDENTIFIER_MACRO(type) |
ocead::reflection::typename_hash(#type) |
Produces OCEAD_REFLECTION_TYPEID_TYPE from the name of the type |
OCEAD_REFLECTION_ADD_PREFIX(symbol) |
_reflection_##symbol |
Macro for modifying the names of additionally generated symbols |
OCEAD_REFLECTION_DEFAULT_PROPERTY_VISIBILITY |
private: |
The default visibility of reflective properties |
OCEAD_REFLECTION_DEFAULT_FUNCTION_VISIBILITY |
public: |
The default visibility of reflective functions |
OCEAD_REFLECTION_DEFAULT_CONSTRUCTOR_VISIBILITY |
public: |
The default visibility of reflective constructors |
OCEAD_REFLECTION_DEFAULT_DESTRUCTOR_VISIBILITY |
public: |
The default visibility of reflective destructors |
You can disable reflection by defining the
OCEAD_NO_REFLECTION
macro. If defined, the macros for reflective symbols expand only to the declaration of the symbols.
- Non-static member fields
- Non-static member functions
- Const-qualified members
- Constructors
- Destructors
- Reference fields
- Functions returning references
- Records
- Functions without return value
- Accessing member fields via offset
- Static member fields
- Static member functions
The code in this repository is distributed under the Boost Software License Version 1.0.
See the code file or the LICENSE file for the full license text.