I am writing a lot of parser code where string_view excels, and have gotten fond of the type. I recently read ArthurO'Dwyer's article std::string_view is a borrow type, where he concludes that string_view (and other 'borrow types') are fine to use as long as they "... appear only as function parameters and for-loop control variables." (with a couple of exceptions).
However, I have lately started to use string_view as return value for functions that convert enum to string (which I use a lot), like this Compiler Explorer:
#include <iostream>
#include <string>
#include <array>
#include <algorithm>
enum class Color
{
red, green, blue, yellow,
last // Must be kept last
};
constexpr std::string_view toString(Color color);
// The rest would normally be in a .cpp file
using cts = std::pair<Color, std::string_view>;
constexpr std::array colorNames = {cts{Color::red, "red color"},
cts{Color::green, "green color"},
cts{Color::blue, "blue color"},
cts{Color::yellow, "yellow color"}};
static_assert(colorNames.size() == static_cast<size_t>(Color::last));
constexpr std::string_view toString(Color color)
{
// Normally calling a library function (which also checks for no match), instead of this:
return std::ranges::find(colorNames, color, &cts::first)->second;
}
int main()
{
auto s1 = toString(Color::green);
auto s2 = toString(Color::blue);
std::cout << s1 << ' ' << s2 << std::endl;
}
The reasons I have for doing it this way are:
- By having it stored in an array as string_view, I can make the entire table constexpr.
- By returning the string_view directly, there is no need of converting the string representation, so the entire function can be constexpr, or at least avoid creating unnecessary strings even when called with a non-constexpr parameter.
- A side effect of having the table constexpr is that I can use static_assert to check that all elements of the enum are in the table, which is really great for catching additions to the enum. I really don't like having to put the 'last' enum value in there, but I don't see a better solution.
So my question is really, is returning the string_view this way unsafe (or UB) in any way, or can I keep on doing this with good conscience?
Alternatively, is there a better (faster/safer) way of solving this general problem of enum-to-string?
Addition: After reading G. Sliepen's very good answer, I'd like to add upon my comment to his answer: I often have the opposite function as well, e.g.:
constexpr Color fromString(string_view str)
{
// No-match handling omitted
return std::ranges::find(colorNames, color, &cts::second)->first;
}
In those situations I really do need the translation as a separate table so that it can be used by both functions. But in many other cases, the function containing a switch statement is the simplest and best.
CodePudding user response:
is returning the string_view this way unsafe (or UB) in any way, or can I keep on doing this with good conscience?
Yes. The way you use it is perfectly ok. The string_view
returned by your toString
function forms a view on data that will remain intact until the program terminates.
Alternatively, is there a better (faster/safer) way of solving this general problem of enum-to-string?
You could make a constexpr
function with a switch
-statement inside it, like so:
constexpr std::string_view toString(Color color)
{
switch (color) {
case Color::red: return "red";
case Color::green: return "green";
...
}
}
There should be no difference in efficiency if the function is evaluated at compile-time. But the compiler can check if you added case
-statements for all the possible Color
s, and if not it will give a warning. There's also no need for a Color::last
this way.
Keeping both the enum
and the std::array
or switch
-statement in sync can be annoying, especially if you have lots of enumeration values. X macros might help here.