9 ms·
Cost of enum-to-string: C++26 reflection vs. the old ways
- HarHarVeryFunny 4mo agoNo 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)
- bluGill 4mo agoC++ conference speakers (including keynotes) are now begging everyone to stop using enum to string in their example. While they are a simple and easy to understand example, reflection is for much more interesting problems. I can't think of any other example that I would type into a comment box or put on a slide.
- surajrmal 4mo agoAnybody the derive traits rust has are a good demo.
- cogman10 4mo agoIt comes up pretty frequently in java. Serialization/Deserialization, adding capabilities based on type, Adding new capabilities to a type, general tuning (for example, adding a timing or logging call onto methods). Almost all the Java web frameworks are giant balls of reflection. Name a function the right way or add the right magic annotation and the framework will autowire it correctly. It's a pretty powerful tool. (IDK if C++'s reflection is as capable, but this is what was enabled by java's reflection).
- david422 4mo ago> Almost all the Java web frameworks are giant balls of reflection. Name a function the right way or add the right magic annotation and the framework will autowire it correctly. I find this to be very powerful, and also very unintuitive/undiscoverable at the same time.
- kuboble 4mo agoReflection is simply a syntax vinegar for duck typing.
- cogman10 4mo agoInitially, but it very quickly becomes discoverable once you are familiar with how things are working. Most frameworks in Java are very similar. The ones that aren't are effectively doing what "expressjs" does in terms of setup, which is still pretty discoverable. Most java frameworks rely on annotations rather than naming schemes which makes everything a lot easier to grok.
- SuperV1234 4mo agoJava reflection is another beast altogether as it is runtime reflection. C++26 reflection is purely compile-time, which not only means it adds zero runtime cost, but also prevents those kind-of-insane use cases you see in Java and C#.
- pjmlp 4mo agoI think C++ devs have to eventually update their knowledge how Java and .NET work when talking about reflection. Yes, originally they only supported runtime reflection. Nowadays they have compile time tooling as well, via plugins, annotation processors, and code generators. Which is exactly how you can have a Spring like frameworks that do all the AOP magic at compile time, for native code with GraalVM or OpenJ9, like Quarkus or Micronaut.
- maccard 4mo agoSerialization is the canonical example. Being able to turn struct MyStruct { int val = 42; string name = "my name"; }; into { "val": 42, // if JSON had integers, and comments of course "name": "my name", } is incredibly powerfuly. If reflection supported attributes (i can't believe it shipped without, honestly), then you could also mark members as [[ignore]] and skip them.
- bluGill 4mo agoIt is powerful, but I'm not sure it is a good idea. Other languages have it, and there is lots of experience in all the ways things go wrong in the real world. I'm inclined to say you should hand write this code because eventually you will discover something weird anyway.
- SuperV1234 4mo agoI think this is a very bad take -- once you write it by hand you have to manually keep it in sync with the actual struct and ensure you made no mistakes. Reflection guarantees 1-1 future-proof mapping with the actual C++ struct, avoids boilerplate, and ensures that the serialization logic is correct.
- bluGill 4mo agoThe protocol is important though, not the internal structure. When you only have exactly one version of a program talking to the same version of itself you don't care. However when you are mixing versions or worse programming language (and thus can't mix structs which are implementation details of your language) the protocol is what matters. That is if you are worried about doing this by hand reflection is not the answer, something like protobuf where your data structures are generated is the answer.
- gpderetta 4mo agoI completely understand your point. Then again you might be able to use reflection to verify that your manually rolled implementation actually serializes all fields.
- theICEBeardk 4mo agoI mean a readable implementation of tuple with minimal overhead is a great case for me (went from around 1.6k lines to approximately 250 lines). I wrote an implementation including the normally difficult to implement tuple_cat based on c++26 within a few hours. My favorite thing is that I will get to remove and replace most of the cryptic template recursion stuff I have with "template for" and maybe a bit of reflection. Debugging the unrolled stuff will be a joy in comparison.
- sagacity 4mo agoOof, that first example (the idiomatic C++26 way) looks so foreign if you're mostly used to C++11.
- ginko 4mo agoIs 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.
- CamouflagedKiwi 4mo agorequires is also new (not sure exactly when that appeared, it's after the last time I wrote C++ in anger) although I think it's fairly clear what it means. I can only guess at the other two. Not familiar with Zig but AFAICT `inline for` is about instructing the compiler to unroll the loop, whereas `template for` means it can be evaluated at compile time and each loop iteration can have a different type for the iteration variable. It's a bit crazy but necessary for reflection to work usefully in the way the language sets it up.
- ginko 4mo agoZig's inline for is also evaluated at comptime: https://ziglang.org/documentation/master/#inline-for https://ziglang.org/documentation/master/#inline-for
- samatman 4mo agoWell yes, but the _effect_ is to unroll the loop for runtime, if the inline-for survives that long. A for loop executed during comptime is just const stuff = comptime stuff: { for (0...8) |i| { // etc, build up some stuff } break :stuff some_stuff; }; The difference is that a comptime block won't leave behind runnable 'residue', only whatever data is constructed for later. An inline for might not leave behind an unrolled loop either, but it can.
- jesgran 4mo ago[dead]
- w4rh4wk5 4mo agoI'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?
- cenamus 4mo agoI mean it's still C++ that's compiled and executed, surely the compiler would be able to provide a way to hook into that?
- usefulcat 4mo agoI don't recall the source, but I don't believe most (any?) c++ compilers implement compile-time code evaluation by compiling and running code. For one thing they are required to disallow all undefined behavior for compile time execution, and some forms of UB only occur when the code is run.
- pjmlp 4mo agoBasically nowadays they ship an interpreter in the box as well.
- SuperV1234 4mo agoNothing that makes it straightforward. Testing via `static_assert` is a good strategy, but it's not debugging. I believe there are some ways of printing custom diagnostics during compilation, but I am not aware of any step-by-step debugging tool that runs at compile-time. In practice, I haven't really needed to ever debug `consteval` functions -- it's quite easy to get the right behavior down thanks to `static_assert`-based testing and thanks to the fact that they do not depend on external state (simpler).
- varispeed 4mo agoWhy people are still using debuggers? I never felt the need for them when doing TDD.
- king_geedorah 4mo agoAnother win for X macros and for C style in general, though the author didn’t declare it as such.
- SuperV1234 4mo agoAuthor here. It isn't a clear "win" at all, there are tradeoffs to each approach.
- spacechild1 4mo agoThe downside is, of course, that it's ugly and very awkward to use. That's the essence of C++: you're basically trading ergonomics for compile times.
- uecker 4mo agoAre X macros awkward? I find them very straightforward and clear.
- spacechild1 4mo agoThe implementation doesn't look too bad, but the usage is terrible: #define E_LIST(X) \ X(V0) X(V1) X(V2) X(V3) DEFINE_ENUM(E, E_LIST) That's not how I want to declare my enums...
- SuperV1234 4mo agoTo be honest there are ways to make that much nicer. I believe that if you use recursive macros using the VA_OPT feature, you should be able to provide enumerators directly to define enum as a list. The underlying machinery implementation is going to be much uglier and complex, though. See https://www.scs.stanford.edu/~dm/blog/va-opt.html https://www.scs.stanford.edu/~dm/blog/va-opt.html
- spacechild1 4mo agoOh, I didn't know about __VA_OPT__(), thanks for that! That looks much nicer indeed, but I still vastly prefer the other solutions, simply because I can just declare regular enums.
- randusername 4mo agoI 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.
- w4rh4wk5 4mo agoI'd argue reflection is very much a feature for libraries. You wouldn't use it directly, but your JSON / YAML serialize is then built on top of it. So are your bindings for scripting engines like Lua.
- bluGill 4mo agoThere are a lot of things that are very very important for a tiny niche. In any non-trivial project you will end up with a lot of custom libraries and some of them really benefit from some obscure feature that no place else in your project would want.
- agentultra 4mo agoAlso nice for UI tooling; game tools, debuggers, etc. Pull apart a struct and display it on screen and not have to patch the UI tool every time you change the struct is pretty nice.
- SuperV1234 4mo agoWe have been able to automatically do this since C++17: https://www.linkedin.com/posts/vittorioromeo_cpp-gamedev-reflection-share-7374128575727271936-cX4j https://www.linkedin.com/posts/vittorioromeo_cpp-gamedev-ref...
- SuperV1234 4mo agoYou can already automatically serialize/deserialize arbitrarily nested structs since C++17 (using Boost.PFR). Since C++20, you can also serialize/deserialize the struct data member names automatically. For many useful use cases, you don't need C++26 reflection at all. E.g. https://www.linkedin.com/posts/vittorioromeo_cpp-gamedev-reflection-share-7374128575727271936-cX4j https://www.linkedin.com/posts/vittorioromeo_cpp-gamedev-ref...
- TZubiri 4mo ago"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.
- SuperV1234 4mo agoLogging, debugging, auto-generation of UIs/editors, etc... This is an extremely common operation and for a good reason.
- jsd1982 4mo agoI 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.
- bluGill 4mo agoI was thinking the same thing. Modules are still not widely used, it is a reasonable guess that there are a lot of optimization opportunities left.
- SuperV1234 4mo agoThat is true, but on the other hand Modules were standardized more than 6 years ago. Promises and claims have been made for longer than that on how Modules would have improved compilation times and made everyone's lives easier. In 2026, I still have to see any real evidence of that, especially when PCH + unity builds are much easier to use (except on damn Bazel, which supports neither) and deliver great results. If after 6+ years of development Modules are still so far behind, it is fair to question if the problem is with the design/implementability of the feature itself.
- spacechild1 4mo ago> it is fair to question if the problem is with the design/implementability of the feature itself. The module story is just insane. How was it possible to get such a big feature into the standard without any working reference implementation? Isn't this the requirement for standard proposals to get accepted? If I compare this with how they treated JeanHeyd and his #embed proposal, the difference is staggering. To me it seems like a few powerful comittee members wanted to get modules into C++20 at any cost. This was just irresponsible.
- bluGill 4mo agoThere was in visual studio which has had it other than minor details.the real problem is tools are needed to make modules work and those needed a lot of work. The work was already partially there because it's the same work that Fortran needs which tools supported but there were just enough details different to be annoying. Fortran modules were something that were always an afterthought and when tools started realizing that this is going to be a big deal, they decided they had to do it right, which took a lot of time too. Maybe you forget Hacker News of 10 years ago, but in 2015-2016, everyone was complaining C++ doesn't have modules and how awful it must be because they're not modules. Now that C++ has modules, they're complaining about how it has modules.
- mentos 4mo agoCurious to see if Epic Games ever refactors their reflection in Unreal Engine to use C++ 26 reflections or not.
- LugosFergus 4mo agoThat'll never happen. The engine's entire serialization system is built around their custom reflection layer and UHT. Not to mention how this would affect licensees. PLUS, they just laid off a bunch of people, and the leftovers are focused on Tim's Verse fiasco. I hate to use jargon here, but there's no "business value" to switching. EDIT: and based on these compilation time results, this would be a major setback for building the engine, which already takes an eternity.
- mentos 4mo agoYea from my discussion/research with ChatGPT it seems compilation times would suffer.
- dataflow 4mo agoI 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?
- SuperV1234 4mo agoIn short, it probes enum values in a pre-defined range (e.g. [-256; 256]), and parses the `__PRETTY_FUNCTION__` macro at compile-time to extract the name of the enumerator. Once you have that in place, you can easily detect duplicates, etc... Of course, there are major limitations, as it's all a big hack: https://github.com/ZXShady/enchantum/blob/main/docs/limitations.md https://github.com/ZXShady/enchantum/blob/main/docs/limitati... Similarly interesting is Boost.PFR, which gives you reflection superpowers since C++14: https://github.com/boostorg/pfr https://github.com/boostorg/pfr
- miguel_martin 4mo agoI 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-simple-keyvalue-file-format 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.
- psyclobe 4mo agoMan 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...
- zxshady 4mo ago[dead]
- cv5005 4mo agoNever 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.
- ironman1478 4mo agoMeta programming in C++ can enable you to remove lots of runtime branching in your code at the cost binary size.
- rddbs 4mo agoOne obvious answer is that people probably don’t want to write a whole parser and wire up new steps in their build pipeline just to do something simple like get the name of enum cases as a string. Without taking a stance on whether in-language meta programming facilities are good or bad, it’s not hard to find examples of cases where people find it useful to have them.
- deleted 4mo ago[deleted]
- pandaman 4mo agoWriting a C++ parser is much harder than a C parser to the point there had been just 3 parsers used among all C++ compilers for quite a while. So you'd need to use some library for parsing. So now you are looking into the library's parser compatibility with the compiler you are using (it might not support the C++ standard you are on at all, it can have bugs preventing it from parsing the code that the compiler parses just fine) and not just on your code but on the library headers you include in your code. What are you going to do when cindex/libclang or whatever chokes on a libstdc++ header? You also have the issue with builtin macros: are they are the same in your library parser? Most likely not. Good luck testing all that. Two-stage compilation is just a bonus on top: you add a sequential dependency in your build graph and if you have enough of these parsing programs you are going to wait till they are all built before your build can go wide.
- vanderZwan 4mo ago> 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?
- SuperV1234 4mo agoThis cost is not significant nowadays, it's the frontend/parsing time. You can also use `#pragma once` which works everywhere, is nicer, and technically needs less work by the compiler, but compilers have optimized for include guards since a long time ago. Some random measurements I found: https://github.com/Return-To-The-Roots/s25client/issues/1073 https://github.com/Return-To-The-Roots/s25client/issues/1073
- vanderZwan 4mo agoYes, I've heard that before, but comments like this one in your linked issue still make me wonder: > at least for gcc and Visual Studio using #pragma once has a significant impact. The fact is, the compiler does not need to continue parsing the whole file when reaching a #pragma once. otherwise the compiler always needs to do it even if the include guard afterwards will avoid double processing of the content afterwards. As written the explanation for these optimizationst suggest that both "pragma once" and include guard optimization still requires opening and closing the file each time an include is encountered, even if you bail after parsing the first line. Is that overhead zero? Or are the optimizations explained poorly and is repeatedly opening/closing the file also avoided? Either way, do you know what causes the slowdown as a result of including <meta>?
- drzaiusx11 4mo agoNo 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.
- Panzerschrek 4mo agoIts 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.
- pjmlp 4mo agoIt has a direct impact on the amount of emails and slack messages I get to reply to.
- SuperV1234 4mo agoUtter BS. Compilation times matter for productivity, developer motivation, iteration speed, CI turnaround time, and so on. I'm sure you wouldn't say "it doesn't matter how long it takes to compile" it if took days. So where do you draw the line? Regardless, it matters.
- Panzerschrek 4mo agoEven days of compilation may be an acceptable price for good optimization, as long as debug builds or builds with minimal optimizations are fast enough.
- Moldoteck 4mo agoour company doesnt do compile on push on the server. It only does it when approved by a subset of ppl. The reason is we have a limited amount of servers and compile takes about 40min/variation. It's very annoying considering at prev job compile took about 10 min in total (project was organized better+ better servers) and there wasn't a limit at all-> compile at each push to gerrit. I'm now trying to migrate from msbuild to cmake+sscache+PCH for std libraries while also trimming unnecessary includes to reduce suffering in the future - if not for me then at least for future developers. So I would say compile time is important for development. It causes other limitations too (like bugfixing becomes a huge commit with several squished fixes together to avoid recompiles, messing up git history or slower context switching when developing several features in parallel)
- psyclobe 4mo agoI made a magic_enum abi stub for this: https://github.com/psyclobe/magic-enum-reflect https://github.com/psyclobe/magic-enum-reflect
- gpderetta 4mo agoI 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.