25 points by todsacerdoti 7 hours ago | | 37 comments

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.

reply

The downside of getting your head around C++ is this leaves your head in a shape that's unfit for any other purpose.

reply

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...

[1] https://github.com/Hirrolot/metalang99

[2] https://hirrolot.github.io/posts/pretty-printable-enumeratio...

[3] https://en.cppreference.com/w/cpp/experimental/reflect

reply

An interface like

  DEF_ENUM(
    EnumName,
    (Name1, value1)
    (Name2, value2)
    ...);
Is also possible. I'm not the greatest fan of PP metaprogramming, but I think this is acceptable.

With additional Magic it might be possible to do separate compilation of the to_string() function.

Funny thing is, I am learning Zig at the moment and had to do just that and was impressed at how clean the solution was:

    const my_enum = enum {
        one,
        two,
        three,

        pub fn str(self: @This()) []const u8 {
            return switch (self) {
                inline else => |tag| @tagName(tag),
            };
        }
    }
Inline else means the switch/case is expanded at compile time.
reply

Why is the switch necessary here, if you only have an else clause for it? Couldn't you just return the tag expression itself?

The "inline else" gets expanded at compile time into a separate case for each enumeration tag.

In V it works automatically for all enums via a builtin str() method:

  my_enum.str()

So far, I'm happy with this simple solution:

  enum _ : u8 {
    kMonday,
    kTuesday,
    ...
    kSize
  };

  constexpr const char* toString[kSize] = {
    "Monday",
    "Tuesday",
    ...
  }

  auto day = toString[kTuesday];
Edit: For `constexpr` replace `std::string` with `const char *`.
reply

>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.

That only works well for dense enums unfortunately.

It is astonishing that such simple and essential feature is still not part of the language.

reply

There is a TS for reflection, but reflection is really not part of the compiled-to-native language paradigm.

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.

A side effect from how ISO works.

This basically parse (at compile time) the __PRETTY_FUNCTION__ or equivalent in a function like

    template <typename Enum, Enum V> constexpr constexpr_string enum_name() {...}
For all possible value of V from 0 to 256.

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>()")

reply

__PRETTY_FUNCTION__ isn't even guaranteed - on MSVC you'll need to resort to __FUNCSIG__ instead.

Example for GCC/Clang/MSVC: https://wandbox.org/permlink/eMt6MxecI4VoJD9m

__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.

I usually just use X macros for this (in C)

reply

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?

reply

String literals are pointers to a read-only data section of the program binary.

A codebase I use at work gets around this by defining all enums in protobufs.

reply

how about this trivial thing ?

   enum class days {
           MONDAY = 1,
           TUESDAY = 2,
           ...
   };

   constexpr const char* str_day(days d) {
           switch (d) {
                   case days::MONDAY: return "MONDAY";
                   case days::TUESDAY: return "TUESDAY";
                   ...
           }
   }
maybe just throw in a 'noexcept' for good measure as well.
reply

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.

language does not really offer a 'loop over all enums' construct. you can push values into a vector/initializer_list/...

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.

I'd probably write a quick python script to generate a function/string table in that case (or I could even do it with a vim macro).

and then check that vim-macro in your code-base somewhere ? sure ^^ ... that will work.

That's literally the first example in the blog...?

That’s hardly DRY though.

sure, can you propose an alternate approach ?

There are a couple of drier alternatives in this blogpost: https://mariusbancila.ro/blog/2023/08/17/how-to-convert-an-e...

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?

reply

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.

> Am I wrong?

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.

> But how often do you do this?

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.

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?

[dead]

reply