Tuesday, May 26, 2026Tech HubAboutContactAdvertiseNewsletter
Back to Home
Compile-Time Map and Compile-Time Mutable Variable with C++26 Reflection

Compile-Time Map and Compile-Time Mutable Variable with C++26 Reflection

B
Blizine Admin
·1 min read·0 views

Compile-Time Map and Compile-Time Mutable Variable with C++26 Reflection - Stack Overflow

Stack Overflow Business Stack Internal: the knowledge intelligence layer that powers enterprise AI.Stack Data Licensing: decades of verified, technical knowledge to boost AI performance and trust.Stack Ads: engage developers where it matters — in their daily workflow.IntroductionHello everyone. I would like to share with you a new method for creating compile-time key-value maps that I discovered while experimenting with the new features introduced in C++26. I will also show a new trick I call the compile-time mutable variable. I believe these methods will be very helpful in your stateful metaprogramming endeavors.Before continuing, you should have an understanding of what a reflection is and its basics. You should also know about the reflection and splice operators. If you do not, I recommend reading §4.1 and §4.2 of the “Reflection for C++26” P2996R13[1] paper. You do not need to know the other sections, as I will explain and reference them as necessary.To understand the new method, it’s best to start by dissecting the compile-time ticket counter example (P2996R13 §3.17). If you understand how it works, you can skip this section.Compile-Time Ticket Counter (P2996R13 §3.17)The examples used in this section can be found on godbolt[2].The compile-time ticket counter is an example of how a compile-time counter could be made using the new features of reflection. The main goal of a compile-time counter is to be able to, during compilation, get the value of the counter and increment the value of the counter. This is useful for things such as automatically giving certain program elements unique numbers during compile-time, instead of manually setting the values and having to make sure that no duplicate numbers are given.The class TU_Ticket has two static consteval functions: latest and increment.The latest function returns the latest (current) value of the counter, and the increment function increments the counter's value by one.To do this, TU_Ticket uses three new functions introduced with reflection in C++26: substitute, is_complete_type, and define_aggregate. These functions are part of the new namespace ‘meta’ and interact with reflections.You may have also noticed the consteval block, with the syntax:consteval {

...contents...

}The contents of a consteval block will be evaluated once during compilation. This is mainly used for the define_aggregate function and functions that will call define_aggregate. For details, see P2996R13 §4.4.1.Substitute“Given a reflection for a template and reflections for template arguments that match that template, substitute returns a reflection for the entity obtained by substituting the given arguments in the template. ”(p2996R13 §4.4.16). Here’s a quick example to illustrate what a substitute does:template struct A; constexpr auto refl_1 = ^^A; constexpr auto refl_2 = substitute(^^A, { ^^int}); static_assert(refl_1 == refl_2); // using substitute is the same as manually writing it // example 1 is_complete_typeThe function is_complete_type, as its name implies, checks if the given reflection represents a complete type.Example:struct A; // incomplete type struct B {}; // complete type static_assert(is_complete_type(^^A) == false); static_assert(is_complete_type(^^B) == true); // example 2 An important thing to know is that if a template is incomplete, then the templated types will be incomplete, unless the explicit template specialization is complete.Example:template struct A; // incomplete class template template struct A {}; // complete explicit template specialization template struct A; // incomplete explicit template specialization static_assert(is_complete_type(^^A) == false); static_assert(is_complete_type(^^A) == false); static_assert(is_complete_type(^^A) == true); // example 3define_aggregate“define_aggregate takes the reflection of an incomplete class/struct/union type and a range of reflections of data member descriptions and completes the given class type with data members as described (in the given order).”(P2996R13 §4.4.19). In short, it takes an incomplete class and completes it. An important thing to note is that define_aggregate only completes the class if the function is called. This means that you can complete a class conditionally.The following two code fragments are functionally the same:With define_aggregate:struct A; static_assert(is_complete_type(^^A) == false); consteval { define_aggregate(^^A, { }); // completes the type } static_assert(is_complete_type(^^A) == true); // example 4aWithout define_aggregate:struct A; static_assert(is_complete_type(^^A) == false); struct A {}; // completes the type static_assert(is_complete_type(^^A) == true); // example 4bIt is important to know that when you complete a templated type, you only complete that specific specialization of the template:template struct A; consteval { define_aggregate(^^A, { }); } static_assert(is_complete_type(^^A) == true); static_assert(is_complete_type(^^A) == false); // example 5aThis code is functionally the same as:template struct A; template struct A {}; static_assert(is_complete_type(^^A) == true); static_assert(is_complete_type(^^A) == false); // example 5bThe define_aggregate function can complete the given class with data members. In our case, we will need to add a single data member, as such:struct A1; consteval { define_aggregate(^^A1, { data_member_spec(^^int, { .name = "value"})}); } struct A2 { int value; }; // example 6In example 6, the classes ‘A1’ and ‘A2’ are the same from the view of their data member.The latest() functionThe latest() function returns the current value of the counter. It does this by linearly searching for the first incomplete template specialization of the template ‘Helper’, where the template parameter is an integer starting from zero and incrementing by one. Once it finds an incomplete template specialization, it knows that the integer is the current value of the counter.Example:consteval{ TU_Ticket::increment(); } constexpr int a = TU_Ticket::latest(); static_assert(a == 1); consteval{ TU_Ticket::increment(); } constexpr int b = TU_Ticket::latest(); static_assert(b==2); //example 7a This code is functionally the same as:template struct Helper {}; constexpr int a = TU_Ticket::latest(); static_assert(a == 1); template struct Helper {}; constexpr int b = TU_Ticket::latest(); static_assert(b == 2); // example 7bLet's go through a small example of what the latest function does. Let's say the counter has been incremented two times, so the latest function should return 2. The function starts by testing the value 0. It checks if the type ‘Helper’ is complete. It is, as it was completed the first time we incremented the counter. So then it checks if the type ‘Helper’ is complete, which was completed in the second increment. Now it checks the type ‘Helper’. It is incomplete, which means that 2 is the current value of the counter. And so it returns the value 2, as it should.Example code:template struct Helper; struct TU_Ticket { static consteval int latest() { int k = 0; while (is_complete_type(substitute(^^Helper, { std::meta::reflect_constant(k)}))) ++k; return k; }

static consteval void increment() { define_aggregate(substitute(^^Helper, { std::meta::reflect_constant(latest())}), {}); } }; consteval { TU_Ticket::increment(); // increment once TU_Ticket::increment(); // increment twice } static_assert(is_complete_type(^^Helper) == true); // class is complete, 0 is not the current value static_assert(is_complete_type(^^Helper) == true); // class is complete, 1 is not the current value static_assert(is_complete_type(^^Helper) == false); // class is incomplete! 2 is the current value, no need to search further static_assert(TU_Ticket::latest() == 2);With this, you should understand that the counter stores information about its previous states by creating new complete template specializations of the incomplete ‘Helper’ class template. With this approach, we have some limitations. We cannot remove or alter any complete template specializations, because we cannot undefine or redefine a class definition. This means that the counter cannot be reset or have its count decremented.The increment() functionThe increment() function simply completes the template specialization with the current count, effectively incrementing the counter.Compile-Time MapThe examples used in this section can be found on godbolt[3].For this compile-time map, both the key and the value will be of type meta::info. This will allow the map to use pretty much anything for its key and value. Want an integer to point to a template? You can do that. Want to map a specific type to a member function? You can do that. The possibilities are endless.You can probably guess that to store the key-value pairs of the map, we will be using class template specialization, where the template parameter will be the key, and the value will be stored in the complete class. But how will we store the value in the class? With define_aggregate, we can define the class to have named data members of a given type. Since we want to store a value of meta::info, we will have to wrap it in a type by having it as a static constexpr data member of type meta::info.example of wrapping a value of type meta::info in a type: template struct storag

📰Originally published at stackoverflow.blog

Comments