C++ For C# Developers: Part 9 – Enumerations
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrapup
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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(red); // 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.
#1 by Jes on July 13th, 2020 ·
Good article thanks :)
One note. C# dont have requirement to add FlagsAttribute to enum to use bitwise operators.
FlagsAttrbute ony slightly change How ToString behaves and thats all :)
#2 by jackson on July 13th, 2020 ·
Thanks for pointing this out. It was overstated in the comparison table at the end, so I updated the article to point out that it’s optional to include
[Flags]
with bit flag enums.#3 by Nathan on November 22nd, 2020 ·
I believe it’s meant to be (red) in the snip below:
char red = Red;
DebugLog(one);
Excellent articles – hugely appreciated; it’s exactly what I needed to revive my rusty old C! :)
#4 by jackson on November 22nd, 2020 ·
I’m glad you’re enjoying the articles. I’ve updated this one with a fix. Thanks for letting me know!
#5 by haldi on February 12th, 2021 ·
shouldn’t the last example be
#6 by jackson on February 13th, 2021 ·
No, I used
enum struct
intentionally to give an example of that. As mentioned earlier in the article:#7 by Berzeger on November 9th, 2024 ·
Hi, thanks for all the articles, much appreciated! Just one note – 1 is not a prime number, so that wasn’t the best example. :)