We’ll continue the series today by discussing enumerations, which is yet-another surprisingly-complex topic in C++. We actually have two closely related concepts of enumerations to go over today, so read on to learn all about both kinds!

Table of Contents

Unscoped Enumerations

The first kind of enumerations in C++ are called “unscoped” enumerations. This is because they don’t introduce a new scope to contain their enumerators, but instead introduce those enumerators to their surrounding scope. Consider this one:

enum Color
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};
 
DebugLog(Red); // 0xff0000

This example shows several aspects of unscoped enumerations. First, defining one is very similar to in C#. We use enum then the enumeration’s name, and put enumerators and their values in curly braces separated by commas. Unlike in C#, we add a semicolon after the closing curly brace.

Second, we see how the Red, Green, and Blue enumerators are put into the surrounding scope rather than inside the Color enum as would have been the case in C#. This means the DebugLog line has Red in scope to read and print out.

Optionally though, we can use C++’s “scope resolution” operator to explicitly access the enumerator:

DebugLog(Color::Red); // 0xff0000

Because the name doesn’t need to be used to access its enumerators, the name of the enum is itself optional:

enum
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};
 
DebugLog(Red); // 0xff0000

Like in C#, the enumerators’ values are optional. They even follow the same rules for default values: the first enumerator defaults to zero and subsequent enumerators default to the previous enumerator’s value plus 1:

enum Prime
{
    One = 1,
    Two,
    Three,
    Five = 5
};
 
DebugLog(One, Two, Three, Five); // 1, 2, 3, 5

Unlike C#, these enumerator values implicitly convert to integer types:

int one = One;
DebugLog(one); // 1

Specifically, the underlying integer type of the enum is chosen from the following list. The smallest type that can hold the largest enumerator’s value is selected.

  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long

If the largest value doesn’t fit in any of these types, the compiler produces an error.

For more control, the underlying integer type can be specified explicitly using the same syntax as in C#:

enum Color : unsigned int
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};

Like in C#, we can cast enumerators to integers. Just note that it’s undefined behavior if that integer is too small to hold the enumerator’s value:

// OK cast to integer
int one = (int)One;
DebugLog(one); // 1
 
// Too big to fit in 1 byte: undefined behavior
char red = Red;
DebugLog(one); // could be anything...

We can also cast integers to enum variables, even if the integer value isn’t one of the enumerators:

// OK cast to enum-typed variable
Prime prime = (Prime)3;
DebugLog(prime); // 3
 
// OK cast to enum-typed variable, even though not a named enumerator
Prime prime = (Prime)4;
DebugLog(prime); // 4

We can also initialize them with a single integer value in curly braces as long as the integer fits in the underlying type and the underlying type has been explicitly stated:

Prime prime{3};

Note that we’ve used the enum name like a type here, just like we could in C#. That means we can write functions like this:

void OutputCharacterToLedDisplay(char ch, Color color)
{
    // ...
}

Passing an arbitrary integer is no longer allowed for color:

OutputCharacterToLedDisplay('J', 0xff0000); // compiler error
 
OutputCharacterToLedDisplay('J', Red); // OK

Like with functions, enums may be declared and referenced without being defined as long as it’s defined later on. When doing this, we have to specify the underlying integer type:

// Declare the enum
enum Color : unsigned int;
 
// Use the enum's name
void OutputCharacterToLedDisplay(char ch, Color color)
{
    // ...
}
 
// Define the enum
enum Color : unsigned int
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};

Both the declaration and the definition are a type just like int or float, so they can be followed by identifiers in order to create variables:

// Declaration
enum Color : unsigned int red, green, blue;
red = Red;
green = Green;
blue = Blue;
DebugLog(red, green, blue); // 0xff0000, 0x00ff00, 0x0000ff
 
// Definition
enum Color : unsigned int
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
} red = Red, green = Green, blue = Blue;
DebugLog(red, green, blue); // 0xff0000, 0x00ff00, 0x0000ff

Finally, there is no special handling of bit flags like C#’s [Flags] attribute. Enumerators of all unscoped enumeration types may simply be used directly:

enum Channel
{
    RedOffset = 16,
    GreenOffset = 8,
    BlueOffset = 0
};
 
unsigned char GetRed(unsigned int color)
{
    return (color & Red) >> RedOffset;
}
 
DebugLog(GetRed(0x123456)); // 0x12
Scoped Enumerations

Fittingly, the other type of enumeration in C++ is called a “scoped” enumeration. As expected, this introduces a new scope which contains the enumerators. They do not spill out into the surrounding scope, so the “scope resolution” operator is required to access them:

enum class Color : unsigned int
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};
 
// Compiler error: Red is not in scope
auto red = Red;
 
// OK, type of the red variable is Color
auto red = Color::Red;

There’s a lot of commonality here: enum, an enum name, an underlying type, enumerator names, enumerator values, the curly braces, and the semicolon at the end. The only difference is the presence of the word class after enum and before the enum’s name. This keyword tells the compiler to make a scoped enumeration instead of an unscoped one. The keyword struct may be used instead and has exactly the same effect as class.

Scoped enumerations behave mostly the same as unscoped enumerations, so we’ll just talk about the handful of differences.

First up, the name of the enum is not optional. This is because such an enum would be pretty useless since its enumerators didn’t get added to the surrounding scope. Without a name to add before the scope resolution operator (::), there’d be no way to access them.

// Compiler error: no name
enum class : unsigned int
{
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff
};
 
// Compiler error: no way to name the enum to access its enumerators
auto red = ???::Red;

Another difference is that the enumerators of a scoped enumeration don’t implicitly convert to integers:

// Compiler error: no implicit conversion
unsigned int red = Color::Red;

Casting is required to convert:

// OK
unsigned int red = (unsigned int)Color::Red;

The choice of underlying type when not explicitly stated is a lot simpler, too: it’s always int:

enum class Numbers
{
    // OK: 1 fits in int
    One = 1,
 
    // Compiler error: too big to fit in an int (assuming int is 32-bit)
    Big = 0xffffffffffffffff
};

Because the underlying type is known to be int, the compiler can use the enumeration type without it being explicitly stated in the declaration:

// OK: underlying type not required for scoped enums
// As usual, the default underlying type is int
enum struct Prime;
 
// OK: definition doesn't need to specify an underlying type either
enum struct Prime
{
    One = 1,
    Two,
    Three,
    Five = 5
};
 
// OK: definition is allowed to specify an underlying type as long as it's int
enum struct Prime : int
{
    One = 1,
    Two,
    Three,
    Five = 5
};

Those are actually all the differences between the two kinds of enumerations. The following table compares and contrasts them with each other and C# enumerations:

Aspect Example Unscoped Scoped C#
Initialization of enumerators One = 1 Optional Optional Optional
Casting enumerators to integers int one = (int)One; Yes Yes Yes
Casting integers to enumerators Prime p = (Prime)4; Yes Yes Yes
Name enum Prime {}; Optional Required Required
Implicit enumerators-to-integer conversion C++: int one = Prime::One
C#: int one = Prime::One
Yes No No
Scope resolution operator C++: Prime::One
C#: Prime.One
Optional Required Required
Implicit underlying type enum E {}; int or larger int int
Underlying type required for declaration enum E; Yes No N/A
(no declarations)
Initialization from integer C++: Prime p{4}
C#: Prime p = 4
Yes Yes No
Immediate variables enum Prime {} p; Yes Yes No
Requirement to use bitwise operators None Casting None
([Flags] optional)
Conclusion

Of the two kinds of enumerations in C++, scoped enumerations are definitely closest to C# enumerations. Still, C++ has unscoped enumerations and they are commonly used. It’s important to know the differences between them, scoped enumerations, and C# enumerations as they have a number of subtle differences to keep in mind.