Native reflection in C++ is coming

Why in 2024 do we have to write an enum cast to a string manually, each custom type needs its own logging function, and bindings to the C++ library require a bunch of repetitive code?

If you have asked these or similar questions, then I have good news for you – soon these problems will be solved. And the best part is – at the language level, and not with a non-standard framework.

Today we are considering proposals of reflection, which are likely to fall into the following standard – C++26.

What is this anyway?

Reflection is the ability of code to examine or even change its structure. Can be divided into 2 types – dynamic and static.

Dynamic reflection is available in runtime (during program execution). For example, Python, where all type information (methods, data) is stored in a data structure accessible to code, so you can, for example, serialize any object without additional code by simply calling json.dumps(object). This works precisely because the dumps function has the ability to iterate over all data fields of any given type.

Static works at compile time. This is a way for code to gain partial access to how the program is represented in the compiler's internal data structures. This is one of the features that is easier to understand by looking at the examples of use – they will be a little lower.

P2996

Main proposal, prescribes the basis for static reflection. Two new operators and a new header are introduced with a set of useful meta functions.

Language changes

  1. New operator ^ – yes, this is a reuse of xor – produces reflection value (reflection) from a type, variable, function, namespace, etc. Reflection has type std::meta::info and is essentially a handle for accessing the internal structure of the reflected “object”.

  2. Splicers – [:R:] – where instead of R the previously created reflection is inserted (std::meta::info). Translates std::meta::info back to type/variable/function/etc.

Library changes

  1. New type std::meta::info – to represent reflection.

  2. Metafunctions in For example: members_of – get a list of members of a class, enumerators_of – a list of constants in the passed enum, offset_of – indentation of the transferred subobject (taking into account padding), is_noexcept – whether the passed function/lambda is noexcept and much more.

Using all this is not at all difficult, especially if you have previously worked with templates.

Examples (taken from crawl)

Receiving reflection and returning to the original type

// отражение
constexpr auto r = ^int;
// int x = 42;
typename[:r:] x = 42;
// char c="*";
typename[:^char:] c="*";

Referring to a class member by name

class S { int i; int j; };

consteval auto member_named(std::string_view name) {
  for (std::meta::info field : nonstatic_data_members_of(^S)) {
    if (name_of(field) == name)
      return field;
  }
}

S s{0, 0};

// s.j = 42;
s.[:member_named("j"):] = 42;
// Ошибка: x не часть класса.
s.[:member_named("x"):] = 0;

Function member_named takes the name of a class member. By using std::meta::nonstatic_data_members_of a list of available class members is requested, for each element in the list its name is requested using std::meta::name_of. The member whose name matches the one passed to the function will be used.

Template function cast enum to a string

template <typename E>
constexpr std::string enum_to_string(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::name_of(e));
    }
  }
  return "<unnamed>";
}

enum Color { red, green, blue };

static_assert(enum_to_string(Color::red) == "red");

The function takes a constant from an arbitrary enum, reflects its type, using std::meta::enumerators_of receives a list of constants for this enum and matches it with the passed constant. The found reflection is transmitted to std::meta::name_of, which returns the name of the constant from the declaration of this enum. About work template for a little lower.

And

IN P2996 (see link at the beginning of the post) there are a lot of other examples, I advise you to at least take a look. The most interesting, subjectively, is the universal formatter. Using it, you can write a template function that can convert any class to a string without additional code. Imagine how many millions of lines of code in the world will become unnecessary just because of this!

P1306

Expansion statements – imagine that you have a collection of objects of different types (for example, tuple) and you want to iterate through it. A regular range loop can't do this, because the variable used to iterate can only be of one type. There are tricks like std::apply and transferring the collection to a template pack, but this requires additional code and is subjectively quite a crutch.

Propozal is offered by a new operator – template for – he is also mentioned in P2996since this functionality greatly simplifies the writing of many meta functions.

Basic example from proposal

auto tup = std::make_tuple(0, ‘a’, 3.14);
template for (auto elem : tup)
  std::cout << elem << std::endl;

This is one of the rare positive cases when it is intuitively clear what a new feature does. Under the hood, everything is also generally not complicated, but it should still be mentioned that the compiler expands this loop into something like the following:

{
  auto elem = std::get<0>(tup);
  std::cout << elem << std::endl;
}
{
  auto elem = std::get<1>(tup);
  std::cout << elem << std::endl;
}
{
  auto elem = std::get<2>(tup);
  std::cout << elem << std::endl;
}

template for This is not a loop in the classical sense, but a way to duplicate a block of code for each element in a collection, which allows elements to be of different types.

P3096

Reflection on function parameters – the feature allows access to information about the function arguments. An example from the proposal, which shows how you can write to output all the arguments of a function without explicitly listing them:

void func(int counter, float factor) {
  template for (constexpr auto e : parameters_of(^func))
    cout << name_of(e) << ": " << [:e:] << "\n";
}

In the meta function proposed by proposal std::meta::parameters_of the current function is passed. std::meta::parameters_of returns a vector with reflections of the function arguments. std::meta::name_of extracts the argument name from reflection, and [:e:] retrieves the value of the argument in the current function call. By the way, this functionality is already available on Godbolt.

P3096 quite a controversial proposal – perhaps that is why it is offered separately from P2996. The fact is that the standard allows you to declare the same function as many times as you like, and with any argument names – the main thing is that the types match. For example:

// file1.h
void func(int value);
// file2.h
void func(int not_a_value);
// file3.cpp
constexpr auto names = meta::parameters_of(^func); // ? 

The question is what name of the int argument should be returned? parameters_of: value or not_a_value? The proposal argues for different solutions, but suggests the following: when calling parameters_of the compiler will check the naming consistency, and if there are discrepancies, then this is a compilation error. This way the existing code does not break, although the scope of the new meta function is slightly limited.

New ideas are cool, but have you tried them in practice?

Yes! There are already two working (but not complete) implementations. It’s still too early to use this in production, but their very presence shows the maturity of the proposal.

  1. IN EDG – this is a commercial compiler, so you won't be able to view the code, but it is available on Godbolt

  2. In open source Clang fork – it is also available on Godboltif you don't want to compile the clang yourself 🙂

What about adoption into the standard?

None of the proposals have yet been accepted by the committee, so theoretically C++26 we may not see them. However, the presence of working implementations and community support allows us to hope that reflection will be included in the next standard. In descending order of likelihood of acceptance: P2996, P1306, P3096. We will keep an eye out for the next standard committee meetings, the next one will be very soon – June 24 in St. Louis.

Interested?

If you want to stay updated on the status of reflection and everything else from the C++ world, subscribe to my telegram channel.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *