Enums in C++

Enumerations, commonly known as enums, are a powerful feature in the C++ programming language. They provide a way to define named integral constants, making code more readable and maintainable.

Understanding Enums in C++

Basics of Enums

Let's say you are writing a program that needs to keep track of whether an apple is red, yellow, or green. If only fundamental types are available, how might you do this?

You might store the color as an integer value, using some kind of implicit mapping (0 = red, 1 = green, 2 = blue):

int main()
{
    int appleColor{ 0 }; // my apple is red
    int shirtColor{ 1 }; // my shirt is green

    return 0;
}

But this isn't at all intuitive:

constexpr int red{ 0 };
constexpr int green{ 1 };
constexpr int blue{ 2 };

int main()
{
    int appleColor{ red };
    int shirtColor{ green };

    return 0;
}

While this is a bit better for reading, the programmer is still left to deduce that appleColor and shirtColor (which are of type int) are meant to hold one of the values defined in the set of color symbolic constants (which are likely defined elsewhere, probably in a separate file).

We can make this program a little more clear by using a type alias:

using Color = int; // define a type alias named Color

// The following color values should be used for a Color
constexpr Color red{ 0 };
constexpr Color green{ 1 };
constexpr Color blue{ 2 };

int main()
{
    Color appleColor{ red };
    Color shirtColor{ green };

    return 0;
}

We are getting closer. Someone reading this code still has to understand that these color symbolic constants are meant to be used with variables of type Color, but at least the type has a unique name now so someone searching for Color would be able to find the set of associated symbolic constants.

However, because Color is just an alias for an int, we still have the problem that nothing enforces proper usage of these color symbolic constants. We can still do something like this:

Color eyeColor{ 8 }; // syntactically valid, semantically meaningless

Enumerations: Compound data type

An enumeration in C++ is declared using the enum keyword, followed by a list of named constants. These constants are typically integers, and each constant is assigned a unique value. C++ supports two kinds of enumerations: un-scoped enumerations and scoped enumerations.

Un-Scoped enumerations

Un-scoped enumerations are defined via the enum keyword. Enumerated types are best taught by example. so let's define an un-scoped enumeration that can hold some color values.

// Define a new unscoped enumeration named Color
enum Color
{
    // Here are the enumerators
    // These symbolic constants define all the possible values this type can hold
    // Each enumerator is separated by a comma, not a semicolon
    red,
    green,
    blue, // trailing comma optional but recommended
}; // the enum definition must end with a semicolon

int main()
{
    // Define a few variables of enumerated type Color
    Color apple { red };   // my apple is red
    Color shirt { green }; // my shirt is green
    Color cup { blue };    // my cup is blue

    Color socks { white }; // error: white is not an enumerator of Color
    Color hat { 2 };       // error: 2 is not an enumerator of Color

    return 0;
}

We start the example by using the enum keyword to tell the compiler that we are defining an un-scoped enumeration, which we have named Color.

Inside a pair of curly braces, we define the enumerators for the Color type: red, green, and blue. These enumerators define the specific values that type Color is restricted to. Each enumerator must be separated by a comma (not a semicolon) – a trailing comma after the last enumerator is optional but recommended for consistency.

The type definition for Color ends with a semicolon. We have now fully defined what enumerated type Color is!

Inside main(), we instantiate three variables of type Color: apple is initialized with the color red, shirt is initialized with the color green, and cup is initialized with the color blue. Memory is allocated for each of these objects. Note that the initializer for an enumerated type must be one of the defined enumerators for that type. The variables socks and hat cause compile errors because the initializers white and 2 are not enumerators of Color.

Recap:

  • An enumeration or enumerated type is the program-defined type itself (e.g. Color)
  • An enumerator is a specific named value belonging to the enumeration (e.g. red)

Naming enumerations and enumerators

By convention, the names of enumerated types start with a capital letter (as do all program-defined types).

Name your enumerators starting with a lower case letter.

The scope of un-scoped enumerations

Un-scoped enumerations are named such because they put their enumerator names into same scope as the enumeration definition itself (as opposed to creating a new scope region like a namespace does).

For example, given this program:

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

int main()
{
    Color apple { red }; // my apple is red

    return 0;
}

The Color enumeration is defined in the global scope. Therefore, all the enumeration names (red, green, and blue) also go into the global scope. This pollutes the global scope and significantly raises the chance of naming collisions.

One consequence of this is that an enumerator name can't be used in multiple enumerations within the same scope:

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

int main()
{
    Color apple { red }; // my apple is red

    return 0;
}

In the above example, both un-scoped enumerations (Color and Feeling) put enumerators with the same name blue into the global scope. This leads to a naming collision and subsequent compile error.

Un-scoped enumerations also provide a named scope region for their enumerators (much like a namespace acts as a named region for the names declared within). This means we can access the enumerators of an un-scoped enumeration as follows:

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

int main()
{
    Color apple { red }; // okay, accessing enumerator from global namespace
    Color raspberry { Color::red }; // also okay, accessing enumerator from scope of Color

    return 0;
}

Most often, un-scoped enumerators are accessed without using the scope resolution.

Avoiding enumerator naming collisions

There are quite a few common ways to prevent un-scoped enumerator naming collisions.

  • One option is to prefix each enumerator with the name of the enumeration itself:
enum Color
{
    color_red,
    color_blue,
    color_green,
};

enum Feeling
{
    feeling_happy,
    feeling_tired,
    feeling_blue, // no longer has a naming collision with color_blue
};

int main()
{
    Color paint { color_blue };
    Feeling me { feeling_blue };

    return 0;
}

This still pollutes the namespace but reduces the chance for naming collisions by making the names longer and more unique.

  • A better option is to put the enumerated type inside something that provides a separated scope region, such as a namespace:
namespace Color
{
    // The names Color, red, blue, and green are defined inside namespace Color
    enum Color
    {
        red,
        green,
        blue,
    };
}

namespace Feeling
{
    enum Feeling
    {
        happy,
        tired,
        blue, // Feeling::blue doesn't collide with Color::blue
    };
}

int main()
{
    Color::Color paint{ Color::blue };
    Feeling::Feeling me{ Feeling::blue };

    return 0;
}

This means we now have to prefix our enumeration and enumerator names with the name of the scoped region.

  • A related option is to use a scoped enumeration (which defines its own scope region). which we will discuss later on.
  • Alternatively, if an enumeration is only used within the body of a single function, the enumeration should be defined inside the function. This limits the scope of the enumeration and its enumerators to just that function. The enumerators of such an enumeration will shadow identically named enumerators defined in the global scope.

Comparing against enumerators

We can use the equality operators (operator == and operator !=) to test whether an enumeration has the value of a particular enumerator or not.

#include <iostream>

enum Color
{
    red,
    green,
    blue,
};

int main()
{
    Color shirt{ blue };

    if (shirt == blue) // if the shirt is blue
        std::cout << "Your shirt is blue!";
    else
        std::cout << "Your shirt is not blue!";

    return 0;
}

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.

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

Un-scoped 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 holds integral values, as you 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 un-scoped 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.

Scoped Enumerations (enum class)

Scoped enumerations work similarly to un-scoped enumerations, but have two primary differences: They are strongly typed (they won't implicitly convert to integers) and strong scoped (the enumerators are only placed into the scope region of the enumeration).

To make a scoped enumeration, we use the keywords enum class. The rest of the scoped enumeration definition is the same as an un-scoped enumeration definition. For ex:

#include <iostream>
int main()
{
    enum class Color // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
    {
        red, // red is considered part of Color's scope region
        blue,
    };

    enum class Fruit
    {
        banana, // banana is considered part of Fruit's scope region
        apple,
    };

    Color color { Color::red }; // note: red is not directly accessible, we have to use Color::red
    Fruit fruit { Fruit::banana }; // note: banana is not directly accessible, we have to use Fruit::banana

    if (color == fruit) // compile error: the compiler doesn't know how to compare different types Color and Fruit
        std::cout << "color and fruit are equal\n";
    else
        std::cout << "color and fruit are not equal\n";

    return 0;
}

enum struct also works in this context, and behaves identically to enum class. However, use of enum struct is non-idiomatic, so avoid its use.

 

 

 

 

enum Days {
    Sunday,    // 0
    Monday,    // 1
    Tuesday,   // 2
    Wednesday, // 3
    Thursday,  // 4
    Friday,    // 5
    Saturday   // 6
};

In this example, the days of the week are represented as constants with values ranging from 0 to 6.

 

Enumerators and Underlying Type

  • Enumerators are the named constants inside an enum. They act as symbolic names for integral values.
  • By default, the underlying type of an enum is int, but it can be explicitly specified:
enum Colors : short {
    Red = 1,
    Green = 2,
    Blue = 4
};

Scoped Enums

C++11 introduced scoped enums, which are more strongly typed and encapsulated:

enum class Status {
    OK,
    Error
};

Here, Status::OK and Status::Error exist within the Status scope, preventing naming conflicts.