#103 – Printing and parsing enums with << and >>

Enumerations are great however printing them out are quite tricky. For unscoped enums, they implicitly convert to an int. For scoped enums, the type safety is enforced. Fortunately, there is a solution!

This Nibble will demonstrate how to use operator overloading to print enumerations and to receive them as input.

Overloading operator<< to print an enumerator

Before we proceed, let’s quickly recap how operator<< works when used for output.

Consider a simple expression like std::cout << 5std::cout has type std::ostream (which is a user-defined type in the standard library), and 5 is a literal of type int.

When this expression is evaluated, the compiler will look for an overloaded operator<< function that can handle arguments of type std::ostream and int. It will find such a function (also defined as part of the standard I/O library) and call it.

Let’s overload << to accept enumerators!

#include <iostream>
#include <string_view>

enum class CoffeeSize {
    Small,
    Medium,
    Large
};

constexpr std::string_view toString(CoffeeSize size){
    switch (size) {
        case CoffeeSize::Small:  return "small";
        case CoffeeSize::Medium: return "medium";
        case CoffeeSize::Large:  return "large";
    }

    return "unknown";
}

std::ostream& operator<<(std::ostream& out, CoffeeSize size)
{
    return out << toString(size);
}

int main(){
    CoffeeSize size { CoffeeSize::Medium };

    std::cout << "Selected size: " << size << '\n';
}

// Terminal output:
Selected size: medium

The overload takes two parameters and outsources the printing to toString().

The stream is passed by reference because streams are not copied.

This line is the key:

return out << toString(size);

The operator does not return a string. It writes the string into the stream, then returns the stream by reference.

Returning std::ostream& is what allows chaining:

std::cout << "Selected size: " << size << '\n'

*std::ostream can be of type std::cout, std::cer, basically any output stream*

Overloading operator>> to input an enumerator

Similarly, we can overload >> to accept an enumerator.

If the user types, large we want CoffeeSize::Large.

#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>

enum class CoffeeSize {
    Small,
    Medium,
    Large
};

constexpr std::string_view toString(CoffeeSize size){
    switch (size) {
        case CoffeeSize::Small:  return "small";
        case CoffeeSize::Medium: return "medium";
        case CoffeeSize::Large:  return "large";
    }

    return "unknown";
}

std::ostream& operator<<(std::ostream& out, CoffeeSize size)
{
    return out << toString(size);
}

std::optional<CoffeeSize> parseCoffeeSize(std::string_view text){
    if (text == "small") {
        return CoffeeSize::Small;
    }

    if (text == "medium") {
        return CoffeeSize::Medium;
    }

    if (text == "large") {
        return CoffeeSize::Large;
    }

    return std::nullopt;
}

std::istream& operator>>(std::istream& in, CoffeeSize& size)
{
    std::string text {};
    in >> text;

    if (std::optional<CoffeeSize> parsed { parseCoffeeSize(text) }) {
        size = *parsed;
        return in;
    }

    in.setstate(std::ios_base::failbit);
    return in;
}

int main(){
    std::istringstream input { "large" };

    CoffeeSize size {};
    input >> size;

    if (input) {
        std::cout << "Order size: " << size << '\n';
    } else {
        std::cout << "Invalid size\n";
    }
}

Terminal output:
Order size: large

The input operator receives the enum by non-const reference:

CoffeeSize& size

That is necessary because the function needs to write the parsed value back into the caller’s object.

The function reads a word from the stream:

in >> text;

Then it attempts to parse that word:

parseCoffeeSize(text)

If parsing succeeds, the enum is assigned:

size = *parsed;

If parsing fails, the stream is placed into the fail state:

in.setstate(std::ios_base::failbit);

This matches normal stream behaviour. Invalid extraction should make the stream fail so the caller can test it with: if (input)or: if (std::cin)

However, there is an easier way to detect invalid input. If the user enters an enumeration that isn’t listed in parseCoffeSize(), std::optional is used

If we order a Mega Large coffee, std::istringstream input { "Mega large" };, the program handles the invalid input gracefully:

Invalid size