# 13.3 — Unscoped enumeration input and output

In the prior lesson (13.2 -- Unscoped enumerations), we mentioned that enumerators are symbolic constants. What we didn’t tell you then is that enumerators are integral symbolic constants. As a result, enumerated types actually hold an integral value.

This is similar to the case with chars (4.11 -- Chars). Consider:

``char ch { 'A' };``

A char is really just a 1-byte integral value, and the character `'A'` gets converted to an integral value (in this case, `65`) and stored.

When we define an enumerator, each enumerator is automatically assigned an integer value based on its position in the enumerator list. By default, the first enumerator is assigned the integral value `0`, and each subsequent enumerator has a value one greater than the previous enumerator:

``````enum Color
{
black, // assigned 0
red, // assigned 1
blue, // assigned 2
green, // assigned 3
white, // assigned 4
cyan, // assigned 5
yellow, // assigned 6
magenta, // assigned 7
};

int main()
{
Color shirt{ blue }; // This actually stores the integral value 2

return 0;
}``````

It is possible to explicitly define the value of enumerators. These integral values can be positive or negative, and can share the same value as other enumerators. Any non-defined enumerators are given a value one greater than the previous enumerator.

``````enum Animal
{
cat = -3,
dog,         // assigned -2
pig,         // assigned -1
horse = 5,
giraffe = 5, // shares same value as horse
chicken,      // assigned 6
};``````

Note in this case, `horse` and `giraffe` have been given the same value. When this happens, the enumerators become non-distinct -- essentially, `horse` and `giraffe` are interchangeable. Although C++ allows it, assigning the same value to two enumerators in the same enumeration should generally be avoided.

Best practice

Avoid assigning explicit values to your enumerators unless you have a compelling reason to do so.

Unscoped enumerations will implicitly convert to integral values

Consider the following program:

``````#include <iostream>

enum Color
{
black, // assigned 0
red, // assigned 1
blue, // assigned 2
green, // assigned 3
white, // assigned 4
cyan, // assigned 5
yellow, // assigned 6
magenta, // assigned 7
};

int main()
{
Color shirt{ blue };

std::cout << "Your shirt is " << shirt << '\n'; // what does this do?

return 0;
}``````

Since enumerated types hold integral values, as you might expect, this prints:

```Your shirt is 2
```

When an enumerated type is used in a function call or with an operator, the compiler will first try to find a function or operator that matches the enumerated type. For example, when the compiler tries to compile `std::cout << shirt`, the compiler will first look to see if `operator<<` knows how to print an object of type `Color` (because `shirt` is of type `Color`) to `std::cout`. It doesn’t.

If the compiler can’t find a match, the compiler will then implicitly convert an unscoped enumeration or enumerator to its corresponding integer value. Because `std::cout` does know how to print an integral value, the value in `shirt` gets converted to an integer and printed as integer value `2`.

Printing enumerator names

Most of the time, printing an enumeration as an integral value (such as `2`) isn’t what we want. Instead, we typically will want to print the name of whatever the enumerator represents (`blue`). But to do that, we need some way to convert the integral value of the enumeration (`2`) into a string matching the enumerator name (`"blue"`).

As of C++20, C++ doesn’t come with any easy way to do this, so we’ll have to find a solution ourselves. Fortunately, that’s not very difficult. The typical way to do this is to write a function that takes an enumerated type as a parameter and then outputs the corresponding string (or returns the string to the caller).

The typical way to do this is to test our enumeration against every possible enumerator:

``````// Using if-else for this is inefficient
void printColor(Color color)
{
if (color == black) std::cout << "black";
else if (color == red) std::cout << "red";
else if (color == blue) std::cout << "blue";
else std::cout << "???";
}``````

However, using a series of if-else statements for this is inefficient, as it requires multiple comparisons before a match is found. A more efficient way to do the same thing is to use a switch statement. In the following example, we will also return our `Color` as a `std::string`, to give the caller more flexibility to do whatever they want with the name (including print it):

``````#include <iostream>
#include <string>

enum Color
{
black,
red,
blue,
};

// We'll show a better version of this for C++17 below
std::string getColor(Color color)
{
switch (color)
{
case black: return "black";
case red:   return "red";
case blue:  return "blue";
default:    return "???";
}
}

int main()
{
Color shirt { blue };

std::cout << "Your shirt is " << getColor(shirt) << '\n';

return 0;
}``````

This prints:

```Your shirt is blue
```

This likely performs better than the if-else chain (switch statements tend to be more efficient than if-else chains), and it’s easier to read too. However, this version is still inefficient, because we need to create and return a `std::string` (which is expensive) every time the function is called.

In C++17, a more efficient option is to replace `std::string` with `std::string_view`. `std::string_view` allows us to return string literals in a way that is much less expensive to copy.

``````#include <iostream>
#include <string_view> // C++17

enum Color
{
black,
red,
blue,
};

constexpr std::string_view getColor(Color color) // C++17
{
switch (color)
{
case black: return "black";
case red:   return "red";
case blue:  return "blue";
default:    return "???";
}
}

int main()
{
constexpr Color shirt{ blue };

std::cout << "Your shirt is " << getColor(shirt) << '\n';

return 0;
}``````

Related content

Constexpr return types are covered in lesson 5.8 -- Constexpr and consteval functions.

Teaching `operator<<` how to print an enumerator

Although the above example functions well, we still have to remember the name of the function we created to get the enumerator name. While this usually isn’t too burdensome, it can become more problematic if you have lots of enumerations. Using operator overloading (a capability similar to function overloading), we can actually teach `operator<<` how to print the value of a program-defined enumeration! We haven’t explained how this works yet, so consider it a bit of magic for now:

``````#include <iostream>

enum Color
{
black,
red,
blue,
};

// Teach operator<< how to print a Color
// Consider this magic for now since we haven't explained any of the concepts it uses yet
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color color)
{
switch (color)
{
case black: return out << "black";
case red:   return out << "red";
case blue:  return out << "blue";
default:    return out << "???";
}
}

int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n'; // it works!

return 0;
}``````

This prints:

```Your shirt is blue
```

For the curious, here’s what the above code is actually doing. When we try to print `shirt` using `std::cout` and `operator<<`, the compiler will see that we’ve overloaded `operator<<` to work with objects of type `Color`. This overloaded `operator<<` function is then called with `std::cout` as the `out` parameter, and our `shirt` as parameter `color`. Since `out` is a reference to `std::cout`, a statement such as `out << "blue"` is really just printing `"blue"` to `std::cout`.

We cover overloading the I/O operators in lesson 21.4 -- Overloading the I/O operators. For now, you can copy this code and replace `Color` with your own enumerated type.

Enumeration size and underlying type (base)

The enumerators of an enumeration are integral constants. The specific integral type used to represent enumerators is called the underlying type (or base).

For unscoped enumerators, the C++ standard does not specify which specific integral type should be used as the underlying type. Most compilers will use type `int` as the underlying type (meaning an unscoped enum will be the same size as an `int`), unless a larger type is required to store the enumerator values.

It is possible to specify a different underlying type. For example, if you are working in some bandwidth-sensitive context (e.g. sending data over a network) you may want to specify a smaller type:

``````#include <cstdint>  // for std::int8_t
#include <iostream>

// Use an 8-bit integer as the enum underlying type
enum Color : std::int8_t
{
black,
red,
blue,
};

int main()
{
Color c{ black };
std::cout << sizeof(c) << '\n'; // prints 1 (byte)

return 0;
}``````

Best practice

Specify the base type of an enumeration only when necessary.

Warning

Because `std::int8_t` and `std::uint8_t` are usually type aliases for char types, using either of these types as the enum base will most likely cause the enumerators to print as char values rather than int values.

Integer to unscoped enumerator conversion

While the compiler will implicitly convert unscoped enumerators to an integer, it will not implicitly convert an integer to an unscoped enumerator. The following will produce a compiler error:

``````enum Pet // no specified base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

int main()
{
Pet pet { 2 }; // compile error: integer value 2 won't implicitly convert to a Pet
pet = 3;       // compile error: integer value 3 won't implicitly convert to a Pet

return 0;
}``````

There are two ways to work around this.

First, you can force the compiler to convert an integer to an unscoped enumerator using `static_cast`:

``````enum Pet // no specified base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

int main()
{
Pet pet { static_cast<Pet>(2) }; // convert integer 2 to a Pet
pet = static_cast<Pet>(3);       // our pig evolved into a whale!

return 0;
}``````

We’ll see an example in a moment where this can be useful.

Second, in C++17, if an unscoped enumeration has a specified base, then the compiler will allow you to list initialize an unscoped enumeration using an integral value:

``````enum Pet: int // we've specified a base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

int main()
{
Pet pet1 { 2 }; // ok: can brace initialize with integer
Pet pet2 (2);   // compile error: cannot direct initialize with integer
Pet pet3 = 2;   // compile error: cannot copy initialize with integer

pet1 = 3;       // compile error: cannot assign with integer

return 0;
}``````

Unscoped enumerator input

Because `Pet` is a program-defined type, the language doesn’t know how to input a Pet using `std::cin`:

``````#include <iostream>

enum Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

int main()
{
Pet pet { pig };
std::cin >> pet; // compile error, std::cin doesn't know how to input a Pet

return 0;
}``````

To work around this, we can read in an integer, and use `static_cast` to convert the integer to an enumerator of the appropriate enumerated type:

``````#include <iostream>

enum Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

int main()
{
std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";

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

Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet

return 0;
}``````

Similar to how we were able to teach `operator<<` to output an enum type above, we can also teach `operator>>` how to input an enum type:

``````#include <iostream>

enum Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};

// Consider this magic for now
// We pass pet by reference so we can have the function modify its value
std::istream& operator>> (std::istream& in, Pet& pet)
{
int input{};
in >> input; // input an integer

pet = static_cast<Pet>(input);
return in;
}

int main()
{
std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";

Pet pet{};
std::cin >> pet; // input our pet using std::cin

std::cout << pet << '\n'; // prove that it worked

return 0;
}``````

Again, consider this a bit of magic for now (since we haven’t explained the concepts behind it yet), but you might find it handy.

In lesson 17.6 -- std::array and enumerations, we show an improved version of `operator>>` that allows input via text (rather than an int).

Quiz time

Question #1

True or false. Enumerators can be:

• Given an integer value

Show Solution

• Given no explicit value

Show Solution

• Given a floating point value

Show Solution

• Given a negative value

Show Solution

• Given a non-unique value

Show Solution

• Initialized with the value of prior enumerators (e.g. magenta = red)

Show Solution