1) all these enums should be enum class
2) magic_enum is very limited. Not only slow but the limit of enum values makes it impractical to use for something like vulkan
>So far, I'm happy with this simple solution: [...] constexpr const char toString[kSize] = { "Monday", "Tuesday", ... } *
But you had to manually code that "toString[] = {"Monday",}"
Your "simple solution" misses the point of the article which is to have the compiler automatically generating the serialized string representation of the enum without a human programmer having to manually code the switch case statements or array lookups and also avoid manually keep the enum & array in sync.
The idea is to have the enum definition itself acting as a "single source of truth" and comparing various language features or libraries to derive the code to write out enum names as strings. In other words, the author is exploring various techniques of pseudo "introspection"/"reflection" for the enum data type.
> Your "simple solution" misses the point of the article which is to have the compiler automatically generating the serialized string representation of the enum without a human programmer having to manually code the switch case statements or array lookups and also avoid manually keep the enum & array in sync.
I agree, in some cases automatic generation might save you some typing.
It might be also painful to maintain, as changing a string requires to change a corresponding enum everywhere in the code.
Just ... write... the function. What is so bad about that? Sure, if you day job is to literally write enums all day then automating that process is worthwhile. But how often do you do this? How much harder is it to just verify the stupid simple function version going through macros hell? Why is this so commonplace in software? I don't see a /s at the end of the blog so the author is unironically advocating for this practice. Am I wrong?
Enums can end up with hundreds of values, and no one wants to do all the tedious code reviews to make sure 'case LONG_VARIABLE_ABRV: return "LONG_VARAIBLE_ABRV"' doesn't have errors in it for every single one.
This is even more important if the enums drive a serializer/deserializer and the hand-coded std::map<string,int> doesn't match the hand coded std::map<int,string> or if either doesn't match the actual enum. Or if you set an alert on the wrong error message because it's spelled differently in the code and in the time-series database.
Source: have replaced production C++ enums and arrays/string-maps with macros after the originals got out of sync after a few years.
Gameplay code often has a lot of frequently changing enums. Audio IDs, events, modifier IDs (surfaces like dirt/grass/etc.), achievement IDs, entity types, resources, ... - these often eventually become tool-generated, as even xmacros style stuff gets annoying to maintain, and need to be exiled into more machine-readable and machine-manipulatable formats.
A lot of projects eventually ditch generating e.g. C++ enums for many of these IDs - the constant rebuilds get too expensive - but that also means you lose out on static compile time validation of your gameplay code...
> What is so bad about that?
Bugs start cropping up in statistically appreciable amounts when the enums grow to 1000s of entries (which might all be found in merely one of 100s of enums) - a forgotten entry here, a typo there, a blithely Ctrl+C Ctrl+Ved entry causing subtly incorrect (de)serialization in this one intractable edge case, causing bug reports that take days to wind through the entire QA pipeline before they reach an actual engineer - who will then have to hope and pray for accurate bug reproduction steps, and that they don't get too misled by their debug tools lying to them... a single bug can easily waste more time than it'd take to convert a lot of enums to macro heaven, and there won't be just a single bug.
> How much harder is it to just verify the stupid simple function version going through macros hell?
One of my open source side projects is winresult, ≈58KLOC of mostly autogenerated rust code and doc comments + ≈18KLOC of mostly autogenerated natvis files, all to handle one enum-like thing: windows HRESULTs. That... would be a lot to hand-write/review/verify/merge.
And these are baby numbers compared to an actual professional gamedev codebase. Attention to detail makes me fairly comfortable with this much hand-generated nonsense in my one man show, but bugs still crop up... and there are people I would dread handing maintainence of such a project over to that I've worked with in a professional capacity, who simply do not care to exercise the same level of care as I do when editing such stuff.
So when someone comes along and modifies the enum definition in the header file while forgetting to update the toStr/fromStr/other helper functions, there won't be a production issue?
Yes, you are doing grunt work that the machine is perfectly capable of doing and in theory has all the knowledge during the compilation stage. Sadly because of C++'s limitations it needs these monstrous macro solutions. Other languages let you use some function or intrinsic or whatever for that.
Sure, if you have a few enums with each a few possible values, by all means do this. However, as soon as you get to have, let us say, some more enums that each have a hundred values or so, not only need to serialize them but also need to deserialize, them, also want to loop over all possible enum values and some such requirements one should start thinking about a smarter solution.
This is very compiler-specific as the actual value of __PRETTY_FUNCTION__ is different for each compiler, and even then, not really specified
(__PRETTY_FUNCTION__ is a builtin macro that expand to a string literal representing the function, for example "the_namespace::enum_name<day, day::Monday>()")
__PRETTY_FUNCTION__ is a GCC extension that was copied by clang. It's not a standard language feature and won't work on something that is not GCC or mimics it closely.
That is a C++ limitation, not a limitation of compiled languages.
Object Pascal had the ability to describe its own types since Delphi 1.0 under Windows 3.1, it is how object serialization works (forms, controls and other components are just object instances serialized to/from disk).
This is data the compiler already has in memory, having some method to save that data to the final executable and a few functions in the standard library to parse that data and give back meaningful results shouldn't really be that much of an effort (and still be very portable), and yet for some reason C and C++ do not provide it even though other languages did decades ago.
Plenty of compiled-to-native language support reflection, Simula being one of the first ones.
Mesa, Eiffel, D, Mesa/Cedar, Delphi, Oberon, Oberon-2, Active Oberon, Clipper, Visual Basic, Component Pascal, Modula-3,... are all compile to native languages with reflection, and the list isn't even exaustive.
There are also other approaches. Macro variants making use of `__VA_ARGS__` would be probably the best trade-off. If you want a slightly more ergonomic syntax, something like Metalang99 [1] will help (and the author even wrote a post about this exact subject [2]). Codegen is another option which may work better than other options depending on the situation and exact implementation strategy. And there is always the Reflection TS [3], which may or may not be incorporated to C++26...
Where is the memory holding the string in the first naive example? To those more familiar with C than C++ it seems like the function returns a pointer to a stack allocation… no?
I've always considered magic_enum pretty useless because if your enum is large enough to need such automated features, it's too large for magic_enum's template hacks so it fails, crashes the compiler, or makes your build times go insane. It only works for toy enums with a few small values.
So instead, I've resigned myself to scripted code generation at build time, and it's one of the things about C++ that makes me feel kind of grossed out.
45 comments
[ 2.2 ms ] story [ 313 ms ] threadBut you had to manually code that "toString[] = {"Monday",}"
Your "simple solution" misses the point of the article which is to have the compiler automatically generating the serialized string representation of the enum without a human programmer having to manually code the switch case statements or array lookups and also avoid manually keep the enum & array in sync.
The idea is to have the enum definition itself acting as a "single source of truth" and comparing various language features or libraries to derive the code to write out enum names as strings. In other words, the author is exploring various techniques of pseudo "introspection"/"reflection" for the enum data type.
I agree, in some cases automatic generation might save you some typing.
It might be also painful to maintain, as changing a string requires to change a corresponding enum everywhere in the code.
Or string_view [1], since it's a literal.
[1] https://en.cppreference.com/w/cpp/string/basic_string_view
This is even more important if the enums drive a serializer/deserializer and the hand-coded std::map<string,int> doesn't match the hand coded std::map<int,string> or if either doesn't match the actual enum. Or if you set an alert on the wrong error message because it's spelled differently in the code and in the time-series database.
Source: have replaced production C++ enums and arrays/string-maps with macros after the originals got out of sync after a few years.
Gameplay code often has a lot of frequently changing enums. Audio IDs, events, modifier IDs (surfaces like dirt/grass/etc.), achievement IDs, entity types, resources, ... - these often eventually become tool-generated, as even xmacros style stuff gets annoying to maintain, and need to be exiled into more machine-readable and machine-manipulatable formats.
A lot of projects eventually ditch generating e.g. C++ enums for many of these IDs - the constant rebuilds get too expensive - but that also means you lose out on static compile time validation of your gameplay code...
> What is so bad about that?
Bugs start cropping up in statistically appreciable amounts when the enums grow to 1000s of entries (which might all be found in merely one of 100s of enums) - a forgotten entry here, a typo there, a blithely Ctrl+C Ctrl+Ved entry causing subtly incorrect (de)serialization in this one intractable edge case, causing bug reports that take days to wind through the entire QA pipeline before they reach an actual engineer - who will then have to hope and pray for accurate bug reproduction steps, and that they don't get too misled by their debug tools lying to them... a single bug can easily waste more time than it'd take to convert a lot of enums to macro heaven, and there won't be just a single bug.
> How much harder is it to just verify the stupid simple function version going through macros hell?
One of my open source side projects is winresult, ≈58KLOC of mostly autogenerated rust code and doc comments + ≈18KLOC of mostly autogenerated natvis files, all to handle one enum-like thing: windows HRESULTs. That... would be a lot to hand-write/review/verify/merge.
https://docs.rs/winresult/ https://github.com/MaulingMonkey/winresult/
Another project: thindx. Bindings for d3dcompiler + d3d9 + xinput have a lot of enums and flags:
• 50 results in 49 files for flags!: https://github.com/search?q=repo%3AMaulingMonkey%2Fthindx%20...
• 77 results in 69 files for enumish!: https://github.com/search?q=repo%3AMaulingMonkey%2Fthindx+en...
• I still do things by hand somtimes, for reasons that elude my recollection: https://github.com/MaulingMonkey/thindx/blob/127d75f9de91f73...
And these are baby numbers compared to an actual professional gamedev codebase. Attention to detail makes me fairly comfortable with this much hand-generated nonsense in my one man show, but bugs still crop up... and there are people I would dread handing maintainence of such a project over to that I've worked with in a professional capacity, who simply do not care to exercise the same level of care as I do when editing such stuff.
Yes, you are doing grunt work that the machine is perfectly capable of doing and in theory has all the knowledge during the compilation stage. Sadly because of C++'s limitations it needs these monstrous macro solutions. Other languages let you use some function or intrinsic or whatever for that.
serialization || deserialization of enum values is a different concern.
it all depends on your need though, and you can spend as little or as much time as you deem necessary.
This is very compiler-specific as the actual value of __PRETTY_FUNCTION__ is different for each compiler, and even then, not really specified
(__PRETTY_FUNCTION__ is a builtin macro that expand to a string literal representing the function, for example "the_namespace::enum_name<day, day::Monday>()")
Example for GCC/Clang/MSVC: https://wandbox.org/permlink/eMt6MxecI4VoJD9m
Object Pascal had the ability to describe its own types since Delphi 1.0 under Windows 3.1, it is how object serialization works (forms, controls and other components are just object instances serialized to/from disk).
This is data the compiler already has in memory, having some method to save that data to the final executable and a few functions in the standard library to parse that data and give back meaningful results shouldn't really be that much of an effort (and still be very portable), and yet for some reason C and C++ do not provide it even though other languages did decades ago.
Mesa, Eiffel, D, Mesa/Cedar, Delphi, Oberon, Oberon-2, Active Oberon, Clipper, Visual Basic, Component Pascal, Modula-3,... are all compile to native languages with reflection, and the list isn't even exaustive.
[1] https://github.com/Hirrolot/metalang99
[2] https://hirrolot.github.io/posts/pretty-printable-enumeratio...
[3] https://en.cppreference.com/w/cpp/experimental/reflect
With additional Magic it might be possible to do separate compilation of the to_string() function.
So instead, I've resigned myself to scripted code generation at build time, and it's one of the things about C++ that makes me feel kind of grossed out.
It was heavily used at a former employer of mine, so definitely a solid production-ready solution.