31 comments

[ 0.18 ms ] story [ 72.4 ms ] thread
Oof, that first example (the idiomatic C++26 way) looks so foreign if you're mostly used to C++11.
Is it? I'm mostly used to (pre-)C++11 and the only unusual operators I see are ^^T (which I presume accesses the metadata info of T) and [:e:] (which I assume somehow casts the enumerator metadata 'e' to a constant value of T).

And template for but I assume that's like inline for like in zig.

I was a fool to assume that the same forces shaping the ugliness of C++ syntax would not also be at work in C++ 26.
You realize c++11 is closer in age to C++98 than C++26?
I was very curious to see what C++ 26 brings to the table, since I haven't used C++ in a while.

When I saw the 'no boilerplate' example, the very first thought that came to my mind:

This is the ugliest, most cryptic and confusing piece of code I've ever seen. Calling this 'no boilerplate' is an insult to the word 'boilerplate'.

Yeah, I can parse it for a minute or two and I mostly get it.

But if given the choice, I'd choose the C-macro implementation (which is 30+ years old) over this, every time. Or the good old switch case where I understand what's going on.

I understand that reflection is a powerful capability for C++, but the template-meta-cryptic-insanity is just too much to invite me back to this version of the language.

I find it quite readable. I can understand what it does even though I haven't written reflection code yet myself.
I wish I understood the reason for the `std::define_static_array`... Why can't `std::meta::enumerators_of` just return something that can be iterated through????
I've been wondering about debug-ability of code using reflection. X-Macros are quite annoying to step through in most debuggers, though possible. While the code in the first example is evaluated fully at compile-time, how would you approach debugging it?
No doubt reflection has been built with other use cases in mind, but it sure would have been nice just to have std::to_string(enum)
Another win for X macros and for C style in general, though the author didn’t declare it as such.
I can't imagine myself using reflection much, but maybe it will eliminate a lot of feature proposals bogging down the committee and they can focus on harder problems.

It would be cool if the stated goal of C++29 was compile times.

"Enum to string"

We've come full circle huh?

Why do you need this, logging? In that case I would rather reflect the logging statement to pribt any variable name, or hell, just write out the string.

If saving for db, maybe store as string, there's more incentive for an enum in the db, if that's a string you might as well. At any rate it doesn't seem a great idea to depend on a variable name, imagine changing a variable name and stuff breaks.

I think the conclusion section should indicate that they are based entirely on GCC 16's behavior and current implementation. We should avoid generalizing one compiler's behavior and performance. Curious how this same test would behave once clang ships C++26 reflection.
Or VC++ if ever, which has the best modules support, but it is still trailing behind in C++23.
Curious to see if Epic Games ever refactors their reflection in Unreal Engine to use C++ 26 reflections or not.
I don't see how a library like Enchantum could handle everything reflection does. (How) does it figure out duplicate enum values, for example? And (how) does it discover arbitrarily large, discontiguous ranges? And (how) does it do these on MSVC?
I agree with some other's in this thread: this is example is not great, but I get why it was used: to compare with X-macros. How about something that would require code-generation e.g. via libclang?

For example, what does https://miguelmartin.com/blog/nim2-review#implementing-a-sim... look like with C++26's std::meta::info?

My guess is: libclang is more suited for this situation if you care about compile times, even if Python is used.

Man that aucks was looking forward to some kind of speed improvement. Using magic enum atm and I guess we'll continue to do so.

C++ build times are hard pill to swallow when migrating from c. This is just another reason we'll probably stick to writing c as t the company where I work. It's like asking someone to give up instant compilation for cleaner easier to read apps?

Also now that we have cleanup handlers in c (destructors) even less of a reason to move...

Never quite understood why people are so obsessed with meta programming capabilities in a language, be it templates, comptime, macros, whatever.

I program mostly in C, if I need 'meta' programming I just write another C program that processes C source code (I've written a simple C parser), then in my build script I build in two stages, build meta program, run it, build rest of program.

Simple, effective, debuggable (the meta program is just normal C), infinite capabilities - can nest this to arbitritary depths, need meta-meta programming? Make a program that generates a meta program.

C++ has templates, which means, that some meta-code generation needs to be executed for arbitrary types. Doing so with an external tool is impossible.
> The header is the cost. Not the reflection. The reflection algorithm is fast – asymptotically ~0.07 ms per enumerator, essentially the same as the hand-rolled switch in the X-macro version (~0.06 ms). What makes reflection look expensive is <meta>: just including it costs ~155 ms per TU over the baseline.

So speaking of old ways, I'm not a C++ dev, but a while ago saw someone comment that they still organize their C++ projects using tips from John Lakos' Large-scale C++ software design from 1997, and that their compile times are incredibly fast. So I decided to find a digital copy on the high seas and read it out of historical curiosity. While I didn't finish it, one wild thing stood out to me: he advised for using redundant external include guards around every include, e.g.

     #ifndef INCLUDED_MATH
     #include <math>
     #define INCLUDED_MATH
     #endif
The reason for this being that (in 1997) every include required that the pre-processor opened the file just to check for an include guard and reading it all the way to the end to find the closing #endif, causing potentially O(N*2) disk read overhead (if anyone feels like verifying this, it's explained on pages 85 to 87).

Again, that was in 1997. I have no idea what mitigations for this problem exist in compilers by now, but I hope at least a few, right?

This conclusion is making me wonder if following that advice still would have a positive impact on compile times today after all though. Surely not, right? Can anyone more knowledgeable about this comment on that?

>...from John Lakos' Large-scale C++ software design from 1997...

I'll just point out that Lakos updated his work with a new edition in 2019:

Large-Scale C++ Volume I: Process and Architecture

and there's scattered evidence that Volume II might be published in Feb. 2027 [1]

Large-Scale C++ Volume II: Design and Implementation

[1]: https://www.amazon.co.uk/Large-Scale-Implementation-Addison-...

(comment deleted)
Oh nice, thanks for the tip! Don't know if I can justify picking up a copy given that I do not work with C++ at all nor with large-scale systems. But I know a few people who might be interested.
No surprise here that the macro + char* approach wins hands down. I'm not really an active C++ user but I did use a VERY similar trick in my custom C code generator DSL (writing in Ruby) just this week. Easy and no "magic" involved.
Its misleading to call it "cost". In the C++ world only runtime cost matters. If using reflection allows to generate faster result code, it doesn't matter how long it takes to compile.
I don't particularly mind the ^^ and [::] sigils, but the 'template for (constexpr auto ...)' is a bit ugly and hard to explain to a beginner.

But interestingly the code can be improved. The issue is that meta::info[1] is a pure compile time object so in the original code we need to statically unroll the loop of the vector that contains it so that we can splice it in in the loop body. But if we convert it to our own objects, then we can use a plain for loop.

  template<class T>
  constexpr static inline auto reflect_type = ^^T; // not really necessary

  template <typename T>
    requires std::is_enum_v<T>
  constexpr std::string_view to_enum_string(T val)
  {
     struct my_string_view { const char * ptr; size_t sz = strlen(ptr); }; 
     static constexpr auto meta = std::define_static_array(
          std::meta::enumerators_of(reflect_type<T>)
        | std::ranges::views::transform(
        [](auto e) { 
          return std::pair{my_string_view{define_static_string(std::meta::identifier_of(e))}, extract<T>(e)}; 
        }));;

     for (auto [name, value] : meta)        
     {
        if (val == value) { return name; }
     }
     return "<unknown>";
  }
This actually generate less code bloat as, if the array is large it will use a plain loop instead of always unrolling. Also the meta array can now be used for as lookup table for dense enums, while I don't think it is doable with the original version. Supposedly GCC should be able to convert a if chain into a switch statement, but it doesn't seem to trigger here [edit: scratch that: GCC does the switch conversion for the original version].

define_static{_array,_string} still feel as unnecessary magic, but hopefully they are only transient and we will be able to use std::vectors directly. Also somehow GCC doesn't let me use std::string_view and I had to introduce an helper string type.

edit: I literally learned everything I know about static reflection in the last 24 hours. It is complicated, but not that complicated.

[1] Not sure why, I suspect they want to avoid being constrained by ABI.