C++ contains many useful fundamental data types (which we introduced in lesson 4.1 -- Introduction to fundamental data types). But these types aren’t always sufficient for the kinds of things we want to do.
For example, let’s say you’re writing a program that needs to keep track of whether an apple is red, yellow, or green, or what color a shirt is (from a preset list of colors). If only fundamental types were available, how might you do this?
You might store the color as an integer value, using some kind of implicit mapping (0 = red , 1 = blue, 2 = green):
1 2 3 4 5 6 7 |
int main() { int appleColor{ 0 }; // my apple is red int shirtColor{ 1 }; // my shirt is blue return 0; } |
But this isn’t at all intuitive, and we’ve already discussed why magic numbers are bad (4.14 -- Const, constexpr, and symbolic constants). We can get rid of the magic numbers by using symbolic constants:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> constexpr int red{ 0 }; constexpr int blue{ 1 }; constexpr int green{ 2 }; int main() { int appleColor{ red }; int shirtColor{ blue }; 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 defined elsewhere).
We can make this program a little more clear by using a type alias:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> using color_t = int; // define a type alias named color_t // The following color values should be used for a color_t constexpr color_t red{ 0 }; constexpr color_t blue{ 1 }; constexpr color_t green{ 2 }; int main() { color_t appleColor{ red }; color_t shirtColor{ blue }; return 0; } |
We’re 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_t
, but at least the type has a unique name now so someone searching for color_t
would be able to find the set of associated symbolic constants.
A bigger problem is that nothing enforces using one of these color symbolic constants. We can still do something like this:
1 |
color_t eyeColor{ 8 }; // syntactically valid, semantically meaningless |
Also, if we debug the any of these variables in our debugger, we’ll only see the integer value of the color (e.g. 0
), not the symbolic meaning (red), which can make it harder to tell if our program is correct.
Fortunately, we can do better. Because selecting a value from a predefined set of values (such as different colors) is so common, C++ provides a compound type to allow us to manage these sets of related values more effectively.
Enumerations
An enumeration (also called an enumerated type or an enum) is program-defined data type where every possible value for the type is defined as an associated symbolic constant (called an enumerator).
C++ supports two kinds of enumerations: unscoped enumerations
, which we’ll cover in this lesson, and scoped enumerations
, which we’ll cover later in this chapter.
Unscoped enumerations
Unscoped enumerations are defined via the enum keyword. Let’s look at an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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 red, blue, green, // 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 { blue }; // my shirt is blue Color cup { green }; // my cup is green Color hat { 8 }; // error: not allowed return 0; } |
Remember, with program-defined types, we need to define what the type looks like before we can use it. So in the first part of this example, we define a a new unscoped enumeration named Color
. Part of this definition includes the enumerators, which are the possible values that the enumeration can hold. In this example, our Color
enumeration can have 3 possible values: red
, blue
, and green
. Note that this definition only tells the compiler what the type looks like -- it does not cause any memory to be allocated.
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 entire enumeration definition must end with a semicolon. Failure to include the semicolon that ends the type definition is a common programmer error, and one that can be hard to debug because the compiler will often error on the line after the type definition.
We then instantiate three objects of type Color
, initializing each one with a different color value (memory is allocated for each these objects).
Naming enumerations and enumerators
By convention, the names of enumerated types start with a capital letter (as do all program-defined types). Providing a name for an enumeration is optional, but highly recommended. Enums without a name are sometimes called anonymous enums, but they are hardly ever used in practice.
New programmers sometimes find the following definition confusing because of the similarity between the type name and variable name:
1 |
Color color { green }; // Color is the type, color is the variable name |
But this is no different than any other variable definition: type comes first (Color
), then variable name (color
), then optional initializer (green
). This naming convention helps ensure that our program-defined type names and our variable names don’t have a naming conflict.
The enumerators must be given names. Unfortunately, there is no common naming convention for enumerator names. Choices include starting with lower case (e.g. red), starting with caps (Red), all caps (RED), all caps with a prefix (COLOR_RED), or prefixed with a “k” and intercapped (kColorRed).
Modern C++ guidelines typically recommend avoiding the all caps naming conventions, as those are typically used for macros. We recommend also avoiding starting with a capital letter, as names beginning with a capital letter are typically reserved for program-defined types.
Best practice
Name your enumerated types starting with a capital letter. Name your enumerators starting with a lower case letter.
Author's note
You’ll see lessons in future chapters that still use the all-caps enumerator naming convention. We’re in the process of cleaning this up.
Enumerated types are distinct
Each enumerated type you create is considered to be a distinct type, meaning the compiler can distinguish it from other types (unlike typedefs or type aliases, which are considered non-distinct from the types they are aliasing).
Because enumerated types are distinct, enumerators defined as part of one enumerated type can’t be used with objects of another enumerated type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> enum Pet { cat, dog, pig, whale, }; enum Color { black, red, blue, }; int main() { Pet myPet { black }; // compile error: black is an enumerator of Color, not Pet Color shirt { pig }; // compile error: pig shirts are strictly disallowed return 0; } |
What are enumerations 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, functions often 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:
1 2 3 4 5 6 7 8 9 10 11 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
enum FileReadResult { readResultSuccess = 0, readResultErrorFileOpen = -1, readResultErrorFileRead = -2, readResultErrorFileParse = -3, }; FileReadResult readFileContents() { if (!openFile()) return readResultErrorFileOpen; if (!readFile()) return readResultErrorFileRead; if (!parsefile()) return readResultErrorFileParse; return readResultSuccess; } |
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.
1 2 3 4 5 6 7 8 |
if (readFileContents() == readResultSuccess) { // 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <string> enum ItemType { itemSword, itemTorch, itemPotion, }; // Teach std::cout how to print a ItemType // Consider this magic for now since we haven't explained any of the concepts it uses yet std::ostream& operator<<(std::ostream& out, ItemType i) { switch (i) { case itemSword: return out << "sword"; case itemTorch: return out << "torch"; case itemPotion: return out << "potion"; } return out; } int main() { ItemType holding{ itemTorch }; std::cout << "You are holding a " << holding << "\n"; return 0; } |
Or alternatively, if you were writing a function to sort a bunch of values:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum SortType { sortForward, sortBackward, }; void sortData(SortType type) { if (type == sortForward) // sort data in forward order else if (type == sortBackward) // sort data in backwards order } |
Many languages use enumerations to define Booleans -- after all, 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.
The scope of unscoped enumerations
Unscoped enumerations were originally named such because they put their enumerator names into the same scope as the enumeration definition itself (as opposed to creating a new scope region like a namespace does).
For example, given this program:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum Color { red, blue, // blue is put into the global namespace green }; 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
enum Color { red, blue, // blue is put into the global namespace green }; enum Feeling { happy, tired, blue // error: naming collision with the above blue }; int main() { Color apple { red }; // my apple is red Feeling me { happy }; // I'm happy right now (even though my program doesn't compile) return 0; } |
In the above example, both unscoped enumerations (Color
and Feeling
) try to put the name blue
into the global scope. This leads to a naming collision and subsequent compile error.
Note: as of C++11, unscoped enumerations now also act as a named scope region for their enumerators (much like a namespace acts as a named scope region for the names declared within). This means we can access the enumerators of an unscoped enumerator as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> enum Color { red, blue, // blue is put into the global namespace green }; 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, unscoped enumerators are accessed without using the scope resolution operator.
Avoiding enumerator naming collisions
There are quite a few common ways to prevent unscoped enumerator naming collisions. One option is to prefix each enumerator with the name of the enumeration itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> 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.
Another option is to put the enum inside a namespace (or something that defines its own named scope, like a class):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> namespace color { // The names Color, red, blue, and green are defined inside namespace color enum Color { red, blue, green }; } 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 name and enumerator names with the name of the scoped region.
A related option is to use an scoped enumeration (which defines its own scope region). We’ll discuss scoped enumerations in the next lesson %Failed lesson reference, id XX%.
Best practice
Prefer to put your enumerations inside a named scope region so they don’t pollute the global namespace.
Quiz time
Question #1
1) Define an enumerated type to choose between the following monster races: orcs, goblins, trolls, ogres, and skeletons.
Question #2
Define a variable of the enumerated type you defined in question 1 and assign it the troll enumerator.
![]() |
![]() |
![]() |
#include<iostream>
using namespace std;
enum Monster{
BOCKY,
KING,
FIREBALL,
BULL,
SCOOBY
};
function(Monster monster){
switch(monster){
case(BOCKY):
cout<<"you choose a RAT!";
break;
case(KING):
cout<<"you choose a LORD!";
case(FIREBALL):
cout<<"you choose a DRAGON!";
break;
case(BULL):
cout<<"you choose a BULL!";
break;
case(SCOOBY):
cout<<"you choose a DOG!";
}
}
main(){
cout<<function(FIREBALL);
}
why i am getting extra valur after my enum type.
Hello Alex,
On the quiz part of this section, what do you mean by " enumerators can be non-unique"?
I know many people have said this, but thanks for all you hard work on the lessons and this website.
I updated the wording to be more clear:
"These integer values can be positive or negative and can share the same value as other enumerators"
Basically, two different enumerators can resolve to the same value.