9.2 — Enumerated types

C++ contains quite a few built in data types. But these types aren’t always sufficient for the kinds of things we want to do. So C++ contains capabilities that allow programmers to create their own data types. These data types are called user-defined data types.

Perhaps the simplest user-defined data type is the enumerated type. An enumerated type (also called an enumeration or enum) is a data type where every possible value is defined as a symbolic constant (called an enumerator). Enumerations are defined via the enum keyword. Let’s look at an example:

// Define a new enumeration named Color
enum Color
{
    // Here are the enumerators
    // These define all the possible values this type can hold
    // Each enumerator is separated by a comma, not a semicolon
    color_black,
    color_red,
    color_blue,
    color_green,
    color_white,
    color_cyan,
    color_yellow,
    color_magenta, // there can be a comma after the last enumerator, but there doesn't have to be a comma
}; // however the enum itself must end with a semicolon

// Define a few variables of enumerated type Color
Color paint = color_white;
Color house(color_blue);
Color apple { color_red };

Defining an enumeration (or any user-defined data type) does not allocate any memory. When a variable of the enumerated type is defined (such as variable paint in the example above), memory is allocated for that variable at that time.

Note that each enumerator is separated by a comma, and the entire enumeration is ended with a semicolon.

Naming enumerations and enumerators

Providing a name for an enumeration is optional, but common. Enums without a name are sometimes called anonymous enums. Enumeration names are often named starting with a capital letter.

Enumerators must be given names, and typically use the same name style as constant variables. Sometimes enumerators are named in ALL_CAPS, but doing so is discouraged, because it risks collisions with preprocessor macro names.

Enumerator scope

Because enumerators are placed into the same namespace as the enumeration, an enumerator name can’t be used in multiple enumerations within the same namespace:

enum Color
{
  red,
  blue, // blue is put into the global namespace
  green
};

enum Feeling
{
  happy,
  tired,
  blue // error, blue was already used in enum Color in the global namespace
};

Consequently, it’s common to prefix enumerators with a standard prefix like animal_ or color_, both to prevent naming conflicts and for code documentation purposes.

Enumerator values

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

enum Color
{
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue, // assigned 2
    color_green, // assigned 3
    color_white, // assigned 4
    color_cyan, // assigned 5
    color_yellow, // assigned 6
    color_magenta // assigned 7
};

Color paint{ color_white };
std::cout << paint;

The cout statement above prints the value 4.

It is possible to explicitly define the value of enumerator. These integer 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.

// define a new enum named Animal
enum Animal
{
    animal_cat = -3,
    animal_dog, // assigned -2
    animal_pig, // assigned -1
    animal_horse = 5,
    animal_giraffe = 5, // shares same value as animal_horse
    animal_chicken // assigned 6
};

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

Best practice

Don’t assign specific values to your enumerators.

Best practice

Don’t assign the same value to two enumerators in the same enumeration unless there’s a very good reason.

Enum type evaluation and input/output

Because enumerated values evaluate to integers, they can be assigned to integer variables. This means they can also be output (as integers), since std::cout knows how to output integers.

int mypet{ animal_pig };
std::cout << animal_horse; // evaluates to integer before being passed to std::cout

This produces the result:

5

The compiler will not implicitly convert an integer to an enumerated value. The following will produce a compiler error:

Animal animal{ 5 }; // will cause compiler error

However, you can force it to do so via a static_cast:

Color color{ static_cast<Color>(5) }; // ugly

The compiler also will not let you input an enum using std::cin:

enum Color
{
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue, // assigned 2
    color_green, // assigned 3
    color_white, // assigned 4
    color_cyan, // assigned 5
    color_yellow, // assigned 6
    color_magenta // assigned 7
};

Color color{};
std::cin >> color; // will cause compiler error

One workaround is to read in an integer, and use a static_cast to force the compiler to put an integer value into an enumerated type:

int inputColor{};
std::cin >> inputColor;

Color color{ static_cast<Color>(inputColor) };

Each enumerated type is considered a distinct type. Consequently, trying to assign enumerators from one enum type to another enum type will cause a compile error:

Animal animal{ color_blue }; // will cause compiler error
If you want to use a different integer type for enumerators, for example to save bandwidth when networking an enumerator, you can specify it at the enum declaration.
// Use an 8 bit unsigned integer as the enum base.
enum Color : std::uint_least8_t
{
    color_black,
    color_red,
    // ...
};

Since enumerators aren’t usually used for arithmetic or comparisons, it’s safe to use an unsigned integer. We also need to specify the enum base when we want to forward declare an enum.

enum Color; // Error
enum Color : int; // Okay

// ...

// Because Color was forward declared with a fixed base, we
// need to specify the base again at the definition.
enum Color : int
{
    color_black,
    color_red,
    // ...
};

As with constant variables, enumerated types show up in the debugger, making them more useful than #defined values in this regard.

Printing enumerators

As you saw above, trying to print an enumerated value using std::cout results in the integer value of the enumerator being printed. So how can you print the enumerator itself as text? One way to do so is to write a function and use an if or switch statement:

enum Color
{
    color_black, // assigned 0
    color_red, // assigned 1
    color_blue, // assigned 2
    color_green, // assigned 3
    color_white, // assigned 4
    color_cyan, // assigned 5
    color_yellow, // assigned 6
    color_magenta // assigned 7
};

void printColor(Color color)
{
    switch (color)
    {
    case color_black:
        std::cout << "Black";
        break;
    case color_red:
        std::cout << "Red";
        break;
    case color_blue:
        std::cout << "Blue";
        break;
    case color_green:
        std::cout << "Green";
        break;
    case color_white:
        std::cout << "White";
        break;
    case color_cyan:
        std::cout << "Cyan";
        break;
    case color_yellow:
        std::cout << "Yellow";
        break;
    case color_magenta:
        std::cout << "Magenta";
        break;
    default:
        std::cout << "Who knows!";
    }
}

Enum allocation and forward declaration

Enum types are considered part of the integer family of types, and it’s up to the compiler to determine how much memory to allocate for an enum variable. The C++ standard says the enum size needs to be large enough to represent all of the enumerator values. Most often, it will make enum variables the same size as a standard int.

Because the compiler needs to know how much memory to allocate for an enumeration, you can only forward declare them when you also specify a fixed base. Because defining an enumeration does not allocate any memory, if an enumeration is needed in multiple files, it is fine to define the enumeration in a header, and #include that header wherever needed.

What are enumerators useful for?

Enumerated types are incredibly useful for code documentation and readability purposes when you need to represent a specific, predefined set of states.

For example, old functions sometimes return integers to the caller to represent error codes when something went wrong inside the function. Typically, small negative numbers are used to represent different possible error codes. For example:

int readFileContents()
{
    if (!openFile())
        return -1;
    if (!readFile())
        return -2;
    if (!parseFile())
        return -3;

    return 0; // success
}

However, using magic numbers like this isn’t very descriptive. An alternative method would be through use of an enumerated type:

enum ParseResult
{
    // We don't need specific values for our enumerators.
    success,
    error_opening_file,
    error_reading_file,
    error_parsing_file
};

ParseResult readFileContents()
{
    if (!openFile())
        return error_opening_file;
    if (!readFile())
        return error_reading_file;
    if (!parsefile())
        return error_parsing_file;

    return success;
}

This is much easier to read and understand than using magic number return values. Furthermore, the caller can test the function’s return value against the appropriate enumerator, which is easier to understand than testing the return result for a specific integer value.

if (readFileContents() == success)
{
    // do something
}
else
{
    // print error message
}

Enumerated types are best used when defining a set of related identifiers. For example, let’s say you were writing a game where the player can carry one item, but that item can be several different types. You could do this:

#include <iostream>
#include <string>

enum ItemType
{
    itemtype_sword,
    itemtype_torch,
    itemtype_potion
};

std::string getItemName(ItemType itemType)
{
    switch (itemType)
    {
      case itemtype_sword:
        return "Sword";
      case itemtype_torch:
        return "Torch";
      case itemtype_potion:
        return "Potion";
    }

    // Just in case we add a new item in the future and forget to update this function
    return "???";
}

int main()
{
    // ItemType is the enumerated type we've defined above.
    // itemType (lower case i) is the name of the variable we're defining (of type ItemType).
    // itemtype_torch is the enumerated value we're initializing variable itemType with.
    ItemType itemType{ itemtype_torch };

    std::cout << "You are carrying a " << getItemName(itemType) << '\n';

    return 0;
}

Or alternatively, if you were writing a function to sort a bunch of values:

enum SortType
{
    sorttype_forward,
    sorttype_backwards
};

void sortData(SortType type)
{
    if (type == sorttype_forward)
        // sort data in forward order
    else if (type == sorttype_backwards)
        // sort data in backwards order
}

Many languages use Enumerations to define booleans. A boolean is essentially just an enumeration with 2 enumerators: false and true! However, in C++, true and false are defined as keywords instead of enumerators.

Quiz time

Question #1

Define an enumerated type to choose between the following monster races: orcs, goblins, trolls, ogres, and skeletons.

Show Solution

Question #2

Define a variable of the enumerated type you defined in question 1 and initialize it with the troll enumerator. Print the value of the variable as an integer.

Show Solution

Question #3

True or false. Enumerators can be:
3a) given an integer value
3b) not assigned a value
3c) given a floating point value
3d) negative
3e) non-unique
3f) initialized with the value of prior enumerators (e.g. color_magenta = color_red)

Show Solution

guest
Your email address will not be displayed
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
372 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments