Search code examples
c++c++17string-view

returning string_view from function


I am writing a lot of parser code where string_view excels, and have become 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:

  1. By having it stored in an array as string_view, I can make the entire table constexpr.
  2. 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.
  3. 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.


Solution

  • 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 Colors, 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.