A fellow engineer at work asked me today how to convert an enum to a string value. This is what I came up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <map> #include <cassert> enum class MyEnum : int { V1, V2, V3, SIZE }; const char* MyEnumToString(MyEnum e) { using MapType = std::map<MyEnum, const char*>; static const MapType kMap = { { MyEnum::V1, "V1" }, { MyEnum::V2, "V2" }, { MyEnum::V3, "V3" }, }; assert(kMap.size() == (int)MyEnum::SIZE); return kMap.at(e); } int main(int argc, char** argv) { std::cout << MyEnumToString(MyEnum::V1) << std::endl; std::cout << MyEnumToString(MyEnum::V2) << std::endl; std::cout << MyEnumToString(MyEnum::V3) << std::endl; return 1; } |
MyEnumToString function has a static map of MyEnum to const char*. In the assert it checks that the MyEnum::SIZE is equal to the size of the map: so you don’t forget to update the map when you update MyEnum with a new entry 😉
For larger enums would unordered_map be a better choice for the mapping data structure in terms of performance? Probably.
UPDATE
Several people on here and reddit suggested that the use of std::map or even std::unordered_map is an overkill. I agree. Though it is a must for non continuous enums (ones that do not start at 0 and do not increment by 1). Below is an updated MyEnumToString function which offers much faster conversion. It uses std::array to hold the string values.
1 2 3 4 5 6 7 8 9 10 11 12 |
const char* MyEnumToString(MyEnum e) { using MapType = std::array<const char*, (size_t)MyEnum::SIZE>; static const MapType kMap = { "V1", "V2", "V3" }; static_assert(kMap.size() == (size_t)MyEnum::SIZE); return kMap.at((MapType::size_type)e); } |
Good Stuff…!!! Liked the part “SIZE” grows naturally.
Ever heard of ‘X macro’?
It is a clever use of macros that helps with this exact problem.
https://youtu.be/hXZrGh9kCVs
Sebastian,
No I have not heard of it. Thank you for bringing it to my attention! I will update the blog with this new information.
BTW, playing around with the X Marco code it looks like it does not work for strongly typed enums 🙁
Hi, your example with std::map and thought about using std::unordered_map seems wrong to me. Why not use C-style array or std::array? Yours enum starts at zero and doesn’t have gaps in it. Marek.
Good point Marek! I’ll update the blog.
Seen your update, good. Why assert? static_assert is better choice here, both left and right part of the == are known at compile time, so no need for run time check.
Also if this is library code I would add assert(e>=0 and e<SIZE); at the beginning, because clients of your library might misbehave. For example when deserializeng the enum from file or network protocol (casting from some kind of integer) and not checking valid range there. Yes they have bug, but the crash is in your code. Assert would tell them it's their fault, not yours.
Thank you for your contribution Marek! I will update the post with your suggestions.
First, thank you for enabling comments.
I miss information about how to use formatting in comments. WordPress now supports markdown, but I’ll assume you have not enabled that. So in a sense this comment is a test: does it work to use HTML formatting, or would markdown be the correct way, or what? And in particular, how to format code: using
HTML
, or
markdown
, or what?
The final function using std::array needlessly has three C style casts. Needless casts are Evil™, and using the C style syntax is generally also Evil™, because with that syntax the cast can change meaning when the code is modified. So even if it can be technically appropriate in a given case, it’s bad as an adopted practice. Since I’m unsure of the formatting I’ll not attempt to post code examples, but merely note that (1) the cast in the type definition is completely unnecessary and can just be removed; (2) the cast in the static assert can avoid a warning but avoiding that warning is IMO better done by completely removing the static assert, which serves no purpose in the current code (it did serve a purpose in the original code); and (3) the cast to size_t of the argument to
at
is better expressed as a cast to the enum’s underlying type, here int (one might define a prefix operator + to do that).
Regarding the use of
at
, as with the
static_cast
that appears to be a remnant from the earlier code, and serves no useful purpose here.
For clarity (so that readers, or yourself at a later time, don’t have to wonder why), replace with ordinary indexing.
Thank you for your great post. I am trying to figure out your static assert statement. I think it will not work if you add a new entry to class enum without adding entry in the string array
The size of the array will still be equal to MyEnum::SIZE and this assert will still be skipped
static_assert(kMap.size() == (size_t)MyEnum::SIZE);
I tried to test it.
If I add VALUE_5 to the enum class I can still compile the code. The size of the array is constant and it is equal to second template parameter. If SIZE in test_enum is increased the size of the array will also be increased
Good catch Slav! Yes looks like the array<…, SIZE> get’s initialized with default values for const char*. I’m not sure how to catch that right now. Let me think about this… if I come up with something I’ll update the code.