But to be able to understand this problem, we need at least some basic knowledge of the design of the reflection mechanism in C++26. Thus we will start just with that.
1. General syntax for reflection
So, the P2996R13 revision of the Reflection proposal states that following (minimal set of) constructs will be added to C++:
- the representation of program elements via constant-expressions producing reflection values (reflections for short) of an opaque type std::meta::info,
- a reflection operator (prefix ^^) that computes a reflection value for its operand construct,
- a number of consteval metafunctions to work with reflections (including deriving other reflections), and
- constructs called splicers to produce grammatical elements from reflections (e.g., [: refl :]).
As we see, we can toggle forth and back between reflection values obtained by ^^ and standard language types and values obtained by [: ... :] (i.e. splicers), as the following example in P2996R13 is showing:
constexpr auto r = ^^int; typename[:r:] x = 42; // Same as: int x = 42; typename[:^^char:] c = '*'; // Same as: char c = '*';
Moreover, the mysteriously sounding splicers can, besides squeezing out the C++ types from std::meta::info values, be also used to access class members, as is shown below:
struct S { int i; int j; }; S s{0, 0}; s.[:^^S::i:] = 44; // Same as: s.i = 44 !!!We are using the object.[: meta::info of a member :] syntax for that. Hard to read? Well, maybe, but better get used to that.
The next ingredient we need are the metafunctions contained in the std::meta namespace.
2. Reflection metafunctions
The metafunctions from the std::meta namespace allow us to to obtain information about classes and class elements. Let's have a closer look at them.
The functions are grouped as follows:
// type queries consteval auto type_of(info r) -> info; consteval auto parent_of(info r) -> info; consteval auto dealias(info r) -> info;Here the awkwardly named dealias() function takes care of stripping off all aliases from a type. For example, for the type Y defined as: using X = int; using Y = X, dealias(^^Y) would give us ^^int.
// object and value queries consteval auto object_of(info r) -> info; consteval auto value_of(info r) -> info;
// name and location consteval auto identifier_of(info r) -> string_view; consteval auto u8identifier_of(info r) -> u8string_view; consteval auto display_string_of(info r) -> string_view; consteval auto u8display_string_of(info r) -> u8string_view; consteval auto source_location_of(info r) -> source_location;and further:
// template queries consteval auto template_of(info r) -> info; consteval auto template_arguments_of(info r) -> vector<info>;
// member queries consteval auto members_of(info type_class) -> vector<info>; consteval auto bases_of(info type_class) -> vector<info>; consteval auto static_data_members_of(info type_class) -> vector<info>; consteval auto nonstatic_data_members_of(info type_class) -> vector<info>; consteval auto subobjects_of(info type_class) -> vector<info>; consteval auto enumerators_of(info type_enum) -> vector<info>;
Note that all of them are returning a vector of std::meta::info values! So this is (at last!) the point where we can discuss the problem advertised in the title. Because, if we have some data structure, we'd like to access its elements one by one, at best in some form of a loop.
3. The template for loop
Of course, the Reflection proposal (P2996R13) shows us how to iterate over a vector of metainfo values, and it should look like this:
template <typename E> constexpr std::string enum2Strg(E value) { template for (constexpr auto e : std::meta::enumerators_of(^^E)) { if (value == [:e:]) return std::string(std::meta::identifier_of(e)); } return "???"; }
So at first sight there shouldn't be a problem, right? Unfortunately, the proposal states then that:
"The implementations notably lack some of the other proposed language features that dovetail well with reflection; most notably, expansion statements are absent."
Hmm, what are expansion statements again? The Expansion Statements proposal (P1306R2) states on its part:
"The template for statement expands the body of the loop once for each element of the tuple. In other words, the expansion statement above is equivalent to the following... "
OK, the missing "expansion statements" refer thus to the template for syntax! What would it do? It would transform a loop over same tuple into a series of operations on each of the tuple's elements.
The reflection proposal follows that caveat by giving an equivalent implementation which can be used instead with current compilers:
[:expand(std::meta::enumerators_of(^^E)):] >> [&]<auto e>{ if (value == [:e:]) { result = std::meta::identifier_of(e); } };
So that's it! The expand construct isn't any standard library function, it's just a workaround which everyone writing about C++26 reflection is copy-pasting in their code and using instead of the unavailable template for construct!
Yes, you are right, just a workaround, there's no need to understand its inner workings or learning it! Just replace it mentally with a foreach().
4. A Goodie
Now, as you (hopefully) can read the new C++ reflection syntax by now, here is a code snippet*** implementing a basic command line option parser. Have fun! 😇
struct MyOpts { std::string file_name = "input.txt"; // "--file_name <string>" int count = 1; // "--count <int>" }; int main(int argc, char* argv[]) { MyOpts opts = parse_options<MyOpts>( std::vector<std::string_view>(argv + 1, argv + argc) ); std::cout << "opts.file=" << opts.file_name << '\n'; std::cout << "opts.count=" << opts.count << '\n'; } template <typename Opts> consteval auto parse_options(std::span<const std::string_view> args) -> Opts { Opts opts; template for (constexpr auto dm : nonstatic_data_members_of(^^Opts)) { auto it = std::ranges::find_if(args, [](std::string_view arg) { return arg.starts_with("--") && arg.substr(2) == identifier_of(dm); }); if (it == args.end()) { continue; // not provided, use default } else if (it + 1 == args.end()) { std::print(stderr, "Option {} is missing a value\n", *it); std::exit(EXIT_FAILURE); } using T = typename[:type_of(dm):]; auto iss = std::ispanstream(it[1]); if (iss >> opts.[:dm:]; !iss) { std::print(stderr, "{} is not a valid {}\n", *it, display_string_of(dealias(^^T))); std::exit(EXIT_FAILURE); } } return opts; }
---
* Herb Sutter's "Trip report: June 2025 ISO C++ standards meeting (Sofia, Bulgaria)": https://herbsutter.com/2025/06/21/trip-report-june-2025-iso-c-standards-meeting-sofia-bulgaria/
** for example; Discover C++26’s compile-time reflection – Daniel Lemire's blog
*** from: "Peering Forward - C++’s Next Decade" by Herb Sutter, Meeting C++ 2024 Conference: https://meetingcpp.com/mcpp/slides/2024/Meeting%20C++%202024606565.pdf