-
Notifications
You must be signed in to change notification settings - Fork 10
Reflection Introductory Tutorial
This tutorial assumes experience with C++, and the presence of a compiler built from either the feature/reflect or feature/metaprogramming branch.
Note: If you are only looking to experiment a bit with the compiler, and go
through these examples, you can use
Compiler Explorer which is updated on a nightly
basis, against feature/metaprogramming
, and can be used with minimal effort.
We provide two header libraries for working with reflection:
These libraries provide a number of functions to allow you to work with compile time reflections.
Note: If your compiler is built against feature/metaprogramming
rather
than feature/reflect
you will see additional functions for working
with metaprograms.
All complete examples unless otherwise specified are assumed to begin with:
#include <experimental/meta>
#include <experimental/compiler>
using namespace std::experimental;
Additionally, it is assumed that all examples are compiled with the compiler flags:
-std=c++2a -freflection
We provide a number of operators for reification. Reification can be thought of as the inverse of reflection. Reification takes a reflection and turns it back into a "program source thing".
unqualid
allows for the formation of unqualified ids from any number of
string literals and integers. It will perform lookup to resolve the name
to a corresponding named entity.
As an example, if we wanted to create the following source:
int variable_1 = 0;
We could do that as follows:
int unqualid("variable_", 1) = 0;
unqualid
can be used anywhere an unqualified id is valid. This means you can
use it not only in declarations, but in expressions. This means we can also
write code such as:
struct x {
int get_zero() { return 0; }
};
struct foo() {
x x_instance;
return x_instance.unqualid("get_zero")();
}
idexpr
similar to unqualid
deals with names. However, idexpr
takes only one argument, a reflection of the named entity to use.
As an example, if we wanted to create the following source:
struct x {
int get_zero() { return 0; }
};
int foo() {
x x_instance;
return x_instance.get_zero();
}
We could do that as follows:
struct x {
int get_zero() { return 0; }
};
int foo() {
x x_instance;
return x_instance.idexpr(reflexpr(x::get_zero))();
}
It's important to note that, unlike unqualid
, idexpr
reification
does not perform lookup. To highlight the difference between the two
consider the following example:
int my_var = 0;
int return_local_var() {
int my_var = 1;
return idexpr(reflexpr(my_var));
}
constexpr meta::info my_var_reflection = reflexpr(my_var);
int return_global_var() {
int my_var = 1;
return idexpr(my_var_reflection);
}
If attempting to implement this same example using unqualid
, we would be
unable to write the return_global_var
function, as its lookup would always
result in use of the local variable, over the global variable.
typename
is used to specify a type from a reflection.
As an example, let's say we wanted to create the following source:
int x = 0;
We could do that with a reflection of an entity of int
type, or the
int
type itself, like so:
typename(reflexpr(int)) x = 0;
valueof
allows extraction of the value of a reflection.
As an example, if we wanted to create the following logic:
int zero = 0;
We could do that as follows:
int zero = valueof(reflexpr(0));
This works with any reflection that has an associated constexpr compatible value. For instance, if we have:
const int global_zero = 0;
We could also write the previous example as:
int zero = valueof(reflexpr(global_zero));
templarg
can be thought of as a combination of both typename
and valueof
specifically for template arguments.
It will infer typename
vs valueof
behavior based upon the type of template
argument in the template declaration.
For instance, let's say you have a reflection:
constexpr int x = 0;
constexpr meta::info refl = reflexpr(x);
If provided the reflection to the following template via templarg
:
template<typename T>
T get_T() {
return T();
}
get_T<templarg(refl)>();
templarg
will infer that what you want is:
get_T<int>();
Similarly, if you provided the reflection to the following template via
templarg
:
template<int V>
int get_V() {
return V;
}
get_V<templarg(refl)>();
templarg
will infer that what you want is:
get_V<0>();
We provide a number of intrinsics for printing. These run during constexpr eval.
__reflect_print
takes any number of string literals, and integers to be
printed during constexpr eval. It's worth noting there is an
implicit newline after printing.
__reflect_pretty_print
takes a reflection, then prints the equivalent source
code for the reflected entity.
Given the following source code:
int zero = 0;
consteval {
__reflect_pretty_print(reflexpr(zero));
}
You can expect output similar to the following:
zero
__reflect_dump
takes a reflection, then dump the AST for the reflected entity.
Given the following source code:
int zero = 0;
consteval {
__reflect_dump(reflexpr(zero));
}
You can expect output similar to the following:
VarDecl 0x555561a83f70 <./example.cpp:5:1, col:12> col:5 referenced zero 'int' cinit
`-IntegerLiteral 0x555561a83fd0 <col:12> 'int' 0
Sadly, there is not yet a constexpr string implementation, this is being worked
on by another group. As an interim solution, we provide the
__concatenate
intrinsic which concatenates any number of string literals,
and integers to be joined inside of a constexpr evaluation context.
For instance:
consteval const char* build_a_new_string() {
const char* a = "foo";
const char* b = "_";
int c = 1;
return __concatenate(a, b, c);
}
It's important to note this only works in constexpr evaluation, and there is no code gen support for this. So, things like the following are invalid:
const char* build_a_new_string() {
const char* a = "foo";
const char* b = "_";
int c = 1;
return __concatenate(a, b, c);
}
The reflections facilities presented here provide a powerful facility for performing a number of common tasks, some of which have not been reasonably achievable in C++. Generating SQL for a model is one such example.
Let's say we're creating a game with persisted player information, given the following player model:
enum class cardinal_direction { NORTH, EAST, SOUTH, WEST };
class player {
float pos_x;
float pos_y;
float pos_z;
int level;
cardinal_direction calculate_cardinal_direction() const;
};
We can write a function using reflection, to generate the SQL to create the table for this player model. For this particular example, we'll be using SQLite dialect, though it's not hard to imagine a system which could work with alternative SQL dialects, or multiple SQL dialects simultaneously.
The SQL we want to generate is as follows:
CREATE TABLE player(pos_x FLOAT, pos_y FLOAT, pos_z FLOAT, level INTEGER);
CREATE TABLE player
The first thing we'll need to do is get the name of the model (class).
To do this we can use meta::name_of
.
consteval const char* generate_table_sql(meta::info clazz) {
const char *table_name = meta::name_of(clazz);
}
( ... );
The next thing we'll need to do is iterate over the class members
using reflection. This task can be accomplished using meta::data_member_range
.
This range in particular is specialized to only return data members.
This is one of many range types that can be found in
our library documentation.
consteval const char* generate_table_sql(meta::info clazz) {
const char *table_name = meta::name_of(clazz);
for (meta::info member : meta::data_member_range(clazz)) {
...
}
}
pos_x FLOAT
It's not enough to just iterate over the members of our player model, we must generate SQL for each column. We can do that by passing the member reflection into another function which generates the column SQL.
Before we can generate the entire column SQL however, we'll need to a way
to generate the appropriate column type name from the C++ type. To do this
we must first get the type using meta::type_of
, once we have the type
we can test it a couple of different ways. The choice between the
two in this case is a matter of personal preference, and the intent of
the design.
Reflection values are comparable, so the first way we can perform type matching is by doing an equivalence test. This has the advantage of being very precise, and only matching if the type is exactly what you're expecting:
consteval const char* to_sql(meta::info type) {
if (type == reflexpr(int)) {
return "INTEGER";
}
if (type == reflexpr(float)) {
return "FLOAT";
}
return "UNKNOWN";
}
The second way we can perform type matching is by interogating the reflection to figure out what is being reflected. This has the advantage of being very flexible and allowing you to write slightly more general code:
consteval const char* to_sql(meta::info type) {
if (meta::is_integral_type(type)) {
return "INTEGER";
}
if (meta::is_floating_point_type(type)) {
return "FLOAT";
}
return "UNKNOWN";
}
Now that we've got a means of generating the column type (to_sql
),
we can use it, along with meta::name_of
and __concatenate
to generate
our desired column SQL as follows:
consteval const char* generate_column_sql(meta::info data_member) {
return __concatenate(meta::name_of(data_member), " ", to_sql(meta::type_of(data_member)));
}
consteval const char* generate_table_sql(meta::info clazz) {
const char *table_name = meta::name_of(clazz);
for (meta::info member : meta::data_member_range(clazz)) {
generate_column_sql(member);
}
}
Now that we've got the individual pieces, we just need to plumb them together with some string concatenations:
consteval const char* to_sql(meta::info type) {
if (type == reflexpr(int)) {
return "INTEGER";
}
if (type == reflexpr(float)) {
return "FLOAT";
}
return "UNKNOWN";
}
consteval const char* generate_column_sql(meta::info data_member) {
return __concatenate(meta::name_of(data_member), " ", to_sql(meta::type_of(data_member)));
}
consteval const char* generate_table_sql(meta::info clazz) {
const char *table_name = meta::name_of(clazz);
const char *table_sql = __concatenate("CREATE TABLE ", table_name, "(");
bool first_seen = false;
for (meta::info member : meta::data_member_range(clazz)) {
if (first_seen)
table_sql = __concatenate(table_sql, ", ");
table_sql = __concatenate(table_sql, generate_column_sql(member));
first_seen = true;
}
return __concatenate(table_sql, ");");
}
enum class cardinal_direction { NORTH, EAST, SOUTH, WEST };
class player {
float pos_x;
float pos_y;
float pos_z;
int level;
cardinal_direction calculate_cardinal_direction() const;
};
To see the generated SQL, we can simply add some constexpr variables:
constexpr const char* table_sql = generate_table_sql(reflexpr(player));
constexpr auto __dummy = __reflect_print(table_sql);