C++26 reflection at compile-time

In today's post, I like to talk about C++26 and one of the probably most impactful features that have been added to the working draft. While C++26 is still some months away from official completion, since the WG21 summer meeting in June we all now know what will be in C++26.

While the new standard will have plenty of great improvements the one that will most likely change a lot is reflection at compile-time! In Sofia we voted seven reflection papers into C++26:

  • P1306R5 Expansion statements
  • P2996R13 Reflection for C++26
  • P3096R12: Function parameter reflection in reflection for C++26
  • P3293R3: Splicing a base class subobject
  • P3394R4: Annotations for reflection
  • P3491R3: define_static_
  • P3560R2: Error handling in reflection

The papers above should give you enough to read for your vacation. I'll leave that theoretical study up to you for now.

Let's talk practical

The main question is, what can you do with that new feature? Well, I'm not the first one who published their ideas.

Steve Downey has an example that parses a JSON string at compile-time creating C++ objects out of it. The direct Compiler Explorer link is godbolt.org/z/YsEK418K6.

The second example comes from Jason Turner which allows you to generate binding to other languages. The direct Compiler Explorer link is godbolt.org/z/6Y17EG984.

While I find both examples great I wanted to show you mine. The issue I tried to solve for years, which also come up in various training classes I thought and even in my own book (Programming with C++20 - Concepts, Coroutines, Ranges, and more) I had to swallow the bitter pill of showing, well not so great code.

Book an in-house C++ training class

Do you like this content?

I'm available for in-house C++ training classes worldwide, on-site or remote. Here is a sample list of my classes:
  • From C to C++
  • Programming with C++11 to C++17
  • Programming with C++20
All classes can be customized to your team's needs. Training services

Reflection, reflection on the wall, what can I do with you all?

What I'm talking about? Enums! Oh no, not how to convert an enum to a string and vice versa. By the way, code for doing that is in that pile of papers above.

No I have at least one other issue which enums: iteration. How often have I wanted to iterate over an enum. Sure there are solutions, mostly macro based and with a lot of rules. For example, only consecutive numbers and a final member called something like Last or MAX. But what when there are holes in an enum? Like

1
enum class Color { Transparent, Red = 2, Green, Blue = 8, Yellow };

Right, that's when the rule kicks in that non-consecutive numbering isn't allowed. Not any more!

Let's make that work, much like in other languages like C# where iterating over the enum values is possible. Without further ado, here it is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
A Helper because this is interesting by itself
template<typename E>
requires std::is_enum_v<E>
constexpr inline auto num_enumerators_of{
  std::meta::enumerators_of(^^E).size()};

B Implementation
template<typename E>
requires std::is_enum_v<E>
consteval auto get_enum_values()
{
  std::array<E, num_enumerators_of<E>> res;

  template for(size_t i{}; constexpr auto& e : std::define_static_array(
                             std::meta::enumerators_of(^^E)))
  {
    res[i++] = [:e:];
  }

  return res;
}

I created a helper variable in A, simply because obtaining the number of enum values in a enum is helpful on its own.

The implementation for the task itself is then in B. You can come up with a different implementation more suitable for lager enums, but this one is sweet and short.

The utility function in action doesn't look special, you can't tell that reflection is going on under the hood:

1
2
3
4
5
for(const auto e : get_enum_values<Color>()) {
  std::print("{} ", std::to_underlying(e));
}

std::println();

You can see, that B returns the strongly typed enum value, that's why the std::to_underlying is required when using the value with std::print. This is a design decision. I decided to stay strongly typed as long as possible.

There are more design considerations, like should get_enum_values be a variable as well? It is constant for each type after all.

At this points I'm not going to explain all the new parts. I just wanted to show you what is possible now.

If you want to play with the code here is the full Compiler Explorer link compiler-explorer.com/z/W8b9xrx5j.

Feel free to reflect a bit on reflection :-)

Andreas

P.S.: In case you wonder whether the implementation of B is correct for an empty enum which gives a zero-sized std::array, the answer is, yes, the code is well formed. One of the advantages of std::array is that there exists a special case for the zero-sized case. A C-style array would be not valid.

Recent posts