I'm going to ignore all the asm considerations because the compiled code will change based on (among other factors):
- the compiler
- the compiler version
- the compiler flags used
- optimizations
- inlining
- caching
- the operating system
- all the other code in the program
In either approach, the compiler could inline everything so that the only space taken up is by the strings themselves. In fact, that's exactly what happens. These two approaches actually result in the same asm when you use the simplest code for both approaches.
First approach:
C++ code:
#include <cstdint>
enum class Num : uint32_t
{
One,
Two,
Three
};
const char* EnumToStr(Num num)
{
switch (num)
{
case Num::One: return "One";
case Num::Two: return "Two";
case Num::Three: return "Three";
}
}
asm (godbolt link):
EnumToStr(Num):
mov edi, edi
mov rax, QWORD PTR CSWTCH.1[0+rdi*8]
ret
.LC0:
.string "One"
.LC1:
.string "Two"
.LC2:
.string "Three"
CSWTCH.1:
.quad .LC0
.quad .LC1
.quad .LC2
Second approach:
C++:
#include <cstdint>
enum class Num : uint32_t
{
One,
Two,
Three,
};
constexpr static const char* table[] =
{
"One",
"Two",
"Three"
};
const char* GetStr(Num num)
{
return table[static_cast<uint32_t>(num)];
}
asm (godbolt link):
GetStr(Num):
mov edi, edi
mov rax, QWORD PTR table[0+rdi*8]
ret
.LC0:
.string "One"
.LC1:
.string "Two"
.LC2:
.string "Three"
table:
.quad .LC0
.quad .LC1
.quad .LC2
Both approaches are converted to an array lookup. The only difference is the name of the array. So, the only question is what code is easiest to read, write, and understand. The question of whether to return const char*, std::string_view, or std::string depends on how you expect the function to be used. If I had to guess, std::string_view is probably best since it is lightweight (consisting of two pointers) and interacts naturally with std::string.
Even including the checks for valid enums results in the same asm:
Scoped enums
The purpose of enum class is to create named constants with no implicit conversions to or from the underlying integer type. This allows you to simplify your code because you don't have to handle out-of-bounds values. The only way to get undefined values of Num is to use static_cast<Num>, and all uses of static_cast deserve close scrutiny due to the loss of type information.
First approach
This approach is the simplest and the one I like better. It doesn't need any other data besides the function itself. The compiler will generate the lookup table, so why write it yourself? Also, the compiler will help you keep the function and the enum synchronized.
Since a break after a return is unreachable, we can make this function more compact.
string EnumToStr(Num num)
{
switch (num)
{
case Num::One: return "One";
case Num::Two: return "Two";
case Num::Three: return "Three";
default: return "";
}
}
The default: case here is unreachable and not necessary, but not all compilers will allow the switch statement without a default: to compile without warnings. The default: case does protect against accidentally skipping Num values, since returning an empty string should have visible consequences in your program. If you ever delete entries from Num the compiler will tell you that, for example, Num::Two is no longer defined.
Second approach
Keeping in mind the purpose of scoped enums, the following check is unnecessary:
uint32_t entryIndex = static_cast<uint32_t>(num);
uint32_t maxNum = static_cast<uint32_t>(Num::MAX);
if (entryIndex < 0 || entryIndex > maxNum)
{
return "";
}
The only way the conditional could evaluate to false is if somebody writes auto x = static_cast<Num>(42); GetStr(x);. The check for valid conversions should happen at the point of the static_cast, so the check inside GetStr() and the entry Num::MAX should be deleted. Also, the fact that you cast to an unsigned integer means the entryIndex < 0 check cannot return false.
string_view GetStr(Num num)
{
return table[static_cast<uint32_t>(num)].str;
}
There's another problem with this. Your second approach includes an extra entry in the enum: Num::MAX. This has a value of 3 and is an out-of-bounds index for table. Even the function with the check would have allowed a call of GetStr(Num::MAX) to access the table with an invalid index. The check should have been if(entryIndex >= maxNum) { return ""; }.
Since you only care about the string representation, I don't see why the Lookup struct has the Num value. It's less typing (and one less function macro) to represent the table as
constexpr static const char* table[] =
{
"Num::One",
"Num::Two",
"Num::Three"
};
Now, the GetStr() function can look like this:
string_view GetStr(Num num)
{
return table[static_cast<uint32_t>(num)];
}
The downside of this approach is that you have less protection if you don't keep the table and the Num enum synchronized. If you have fewer entries in table than Num, your program will at-best crash after trying to access data beyond the end of the array. If you have more entries in table than Num, then those extra entries uselessly take up space in the program. This is why I prefer the first approach.
using namespace std;?std::endl? You don't have to break out all the bad ideas. \$\endgroup\$