Skip to content

Reflection Introductory Tutorial

Wyatt Childers edited this page Jul 10, 2020 · 5 revisions

Preface

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.

Background

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.

Example Preface

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

Reifiers

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

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

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

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

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

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>();

Printing

We provide a number of intrinsics for printing. These run during constexpr eval.

reflect_print

__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

__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

__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

Constexpr Strings

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

Generating SQL

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

Generating the Table Name

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

Iterating Over Class Members

( ... );

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

Generating Columns

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.

Generating Column Type

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.

By Equivalence

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";
}
By Interogation

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";
}

Creating the Column SQL

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

Putting It All Together

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