Converting an enumeration to and from a string

In the prior lesson, we showed an example like this:

#include <iostream>

enum Difficulty
{
    easy,    // 0
    medium,  // 1
    hard,    // 2
};

int main()
{
    Difficulty gameLevel{ hard };

    std::cout << "Your difficulty level is " << gameLevel << '\n';

    return 0;
}

This prints:

Your difficulty level is 2

Because operator<< doesn't know how to print a Difficulty, the compiler will implicitly convert Difficulty into an integral value and print that instead.

Most of the time, printing an enumeration as an integral value (such as 2) isn't what we want. Instead, we typically want to print the name of whatever the enumerator represents (e.g. hard). C++ doesn't come with an out-of-the-box way to do this, so we'll have to find a solution ourselves. Fortunately, that's not very difficult.

Getting the name of an enumerator

The typical way to get the name of an enumerator is to write a function that allows us to pass in an enumerator and returns the enumerator's name as a string. But that requires some way to determine which string should be returned for a given enumerator.

There are two common ways to do this.

In an earlier lesson, we noted that a switch statement can switch on either an integral value or an enumerated value. In the following example, we use a switch statement to select an enumerator and return the appropriate difficulty string literal for that enumerator:

#include <iostream>
#include <string_view>

enum Difficulty
{
    easy,
    medium,
    hard,
};

constexpr std::string_view getDifficultyName(Difficulty difficulty)
{
    switch (difficulty)
    {
    case easy:   return "easy";
    case medium: return "medium";
    case hard:   return "hard";
    default:     return "???";
    }
}

int main()
{
    constexpr Difficulty gameLevel{ hard };

    std::cout << "Your difficulty level is " << getDifficultyName(gameLevel) << '\n';

    return 0;
}

This prints:

Your difficulty level is hard

In the above example, we switch on difficulty, which holds the enumerator we passed in. Inside the switch, we have a case-label for each enumerator of Difficulty. Each case returns the name of the appropriate difficulty as a C-style string literal. This C-style string literal gets implicitly converted into a std::string_view, which is returned to the caller. We also have a default case which returns "???", in case the user passes in something we didn't expect.

Reminder
Because C-style string literals exist for the entire program, it's okay to return a `std::string_view` that is viewing a C-style string literal. When the `std::string_view` is copied back to the caller, the C-style string literal being viewed will still exist.

The function is constexpr so that we can use the difficulty's name in a constant expression.

While this lets us get the name of an enumerator, if we want to print that name to the console, having to do std::cout << getDifficultyName(gameLevel) isn't quite as nice as std::cout << gameLevel. We'll teach std::cout how to print an enumeration in an upcoming lesson.

The second way to solve the program of mapping enumerators to strings is to use an array. We cover this in a later lesson on arrays.

Unscoped enumerator input

Now let's take a look at an input case. In the following example, we define a Direction enumeration. Because Direction is a program-defined type, the language doesn't know how to input a Direction using std::cin:

#include <iostream>

enum Direction
{
    north,   // 0
    south,   // 1
    east,    // 2
    west,    // 3
};

int main()
{
    Direction heading{ north };
    std::cin >> heading; // compile error: std::cin doesn't know how to input a Direction

    return 0;
}

One simple way to work around this is to read in an integer, and use static_cast to convert the integer to an enumerator of the appropriate enumerated type:

#include <iostream>
#include <string_view>

enum Direction
{
    north,   // 0
    south,   // 1
    east,    // 2
    west,    // 3
};

constexpr std::string_view getDirectionName(Direction direction)
{
    switch (direction)
    {
    case north: return "north";
    case south: return "south";
    case east:  return "east";
    case west:  return "west";
    default:    return "???";
    }
}

int main()
{
    std::cout << "Enter a direction (0=north, 1=south, 2=east, 3=west): ";

    int input{};
    std::cin >> input; // input an integer

    if (input < 0 || input > 3)
        std::cout << "You entered an invalid direction\n";
    else
    {
        Direction heading{ static_cast<Direction>(input) }; // static_cast our integer to a Direction
        std::cout << "You entered: " << getDirectionName(heading) << '\n';
    }

    return 0;
}

While this works, it's a bit awkward. Also note that we should only static_cast<Direction>(input) once we know input is in range of the enumerator.

Getting an enumeration from a string

Instead of inputting a number, it would be nicer if the user could type in a string representing an enumerator (e.g. "north"), and we could convert that string into the appropriate Direction enumerator. However, doing this requires us to solve a couple of challenges.

First, we can't switch on a string, so we need to use something else to match the string the user passed in. The simplest approach here is to use a series of if-statements.

Second, what Direction enumerator should we return if the user passes in an invalid string? One option would be to add an enumerator to represent "none/invalid", and return that. However, a better option is to use std::optional here.

Related Content
We cover `std::optional` in an earlier lesson.
```cpp #include #include // for std::optional #include #include

enum Direction { north, // 0 south, // 1 east, // 2 west, // 3 };

constexpr std::string_view getDirectionName(Direction direction) { switch (direction) { case north: return "north"; case south: return "south"; case east: return "east"; case west: return "west"; default: return "???"; } }

constexpr std::optional<Direction> getDirectionFromString(std::string_view sv) { // We can only switch on an integral value (or enum), not a string // so we have to use if-statements here if (sv == "north") return north; if (sv == "south") return south; if (sv == "east") return east; if (sv == "west") return west;

return {};

}

int main() { std::cout << "Enter a direction: north, south, east, or west: "; std::string userInput{}; std::cin >> userInput;

std::optional<Direction> heading{ getDirectionFromString(userInput) };

if (!heading)
    std::cout << "You entered an invalid direction\n";
else
    std::cout << "You entered: " << getDirectionName(*heading) << '\n';

return 0;

}


In the above solution, we use a series of if-else statements to do string comparisons. If the user's input string matches an enumerator string, we return the appropriate enumerator. If none of the strings match, we return `{}`, which means "no value".

**Advanced note:** The above solution only matches lower case letters. If you want to match any letter case, you can use the following function to convert the user's input to lower case:


```cpp
#include <algorithm> // for std::transform
#include <cctype>    // for std::tolower
#include <iterator>  // for std::back_inserter
#include <string>
#include <string_view>

// This function returns a std::string that is the lower-case version of the std::string_view passed in.
// Only 1:1 character mapping can be performed by this function
std::string toASCIILowerCase(std::string_view sv)
{
    std::string lower{};
    std::transform(sv.begin(), sv.end(), std::back_inserter(lower),
        [](char c)
        {
            return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        });
    return lower;
}

This function steps through each character in std::string_view sv, converts it to a lower case character using std::tolower() (with the help of a lambda), and then appends that lower-case character to lower.

Similar to the output case, it would be better if we could just std::cin >> heading. We'll cover this in an upcoming lesson.

Summary

Getting enumerator names: C++ doesn't automatically print enumerator names - it converts enumerations to integers by default. To print enumerator names, create a function (typically using a switch statement) that maps each enumerator to its corresponding string representation. Return the name as a std::string_view for efficiency, and make the function constexpr for use in constant expressions.

Input via integer conversion: For user input, one approach is to read an integer and use static_cast to convert it to the appropriate enumerator. Always validate the input is in range before performing the cast to avoid undefined behavior.

String-to-enumerator conversion: For more user-friendly input, accept string input and use a series of if-statements to match the string to the appropriate enumerator. Use std::optional as the return type to handle invalid input gracefully, returning an empty optional when no match is found.

Case-insensitive matching: To support any letter case in string input, first convert the user's input to lowercase using std::transform and std::tolower before performing string comparisons.

These patterns form the foundation for making enumerations more usable in interactive programs. In the next lesson, we'll explore how to overload I/O operators to provide even cleaner enumeration input and output syntax.