Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Type-Safe Enumerations with enum class
Use enum class for strongly-typed enumerations that prevent implicit conversions.
Scoped enumerations (enum classes)
Although unscoped enumerations are distinct types in C++, they are not type safe, and in some cases will allow you to do things that don't make sense. Consider the following case:
#include <iostream>
int main()
{
enum Difficulty
{
easy,
hard,
};
enum Volume
{
quiet,
loud,
};
Difficulty gameLevel{ easy };
Volume soundLevel{ quiet };
if (gameLevel == soundLevel) // The compiler will compare gameLevel and soundLevel as integers
std::cout << "gameLevel and soundLevel are equal\n"; // and find they are equal!
else
std::cout << "gameLevel and soundLevel are not equal\n";
return 0;
}
This prints:
gameLevel and soundLevel are equal
When gameLevel and soundLevel are compared, the compiler will look to see if it knows how to compare a Difficulty and a Volume. It doesn't. Next, it will try converting Difficulty and/or Volume to integers to see if it can find a match. Eventually the compiler will determine that if it converts both to integers, it can do the comparison. Since gameLevel and soundLevel are both set to enumerators that convert to integer value 0, gameLevel will equal soundLevel.
This doesn't make sense semantically since gameLevel and soundLevel are from different enumerations and are not intended to be comparable. With standard enumerators, there's no easy way to prevent this.
Because of such challenges, as well as the namespace pollution problem (unscoped enumerations defined in the global scope put their enumerators in the global namespace), the C++ designers determined that a cleaner solution for enumerations would be of use.
Scoped enumerations
That solution is the scoped enumeration (often called an enum class in C++ for reasons that will become obvious shortly).
Scoped enumerations work similarly to unscoped enumerations, but have two primary differences: They won't implicitly convert to integers, and the enumerators are only placed into the scope region of the enumeration (not into the scope region where the enumeration is defined).
To make a scoped enumeration, we use the keywords enum class. The rest of the scoped enumeration definition is the same as an unscoped enumeration definition. Here's an example:
#include <iostream>
int main()
{
enum class Difficulty // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
{
easy, // easy is considered part of Difficulty's scope region
hard,
};
enum class Volume
{
quiet, // quiet is considered part of Volume's scope region
loud,
};
Difficulty gameLevel{ Difficulty::easy }; // note: easy is not directly accessible, we have to use Difficulty::easy
Volume soundLevel{ Volume::quiet }; // note: quiet is not directly accessible, we have to use Volume::quiet
if (gameLevel == soundLevel) // compile error: the compiler doesn't know how to compare different types Difficulty and Volume
std::cout << "gameLevel and soundLevel are equal\n";
else
std::cout << "gameLevel and soundLevel are not equal\n";
return 0;
}
This program produces a compile error on line 19, since the scoped enumeration won't convert to any type that can be compared with another type.
The `class` keyword (along with the `static` keyword), is one of the most overloaded keywords in the C++ language, and can have different meanings depending on context. Although scoped enumerations use the `class` keyword, they aren't considered to be a "class type" (which is reserved for structs, classes, and unions).
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.
Scoped enumerations define their own scope regions
Unlike unscoped enumerations, which place their enumerators in the same scope as the enumeration itself, scoped enumerations place their enumerators only in the scope region of the enumeration. In other words, scoped enumerations act like a namespace for their enumerators. This built-in namespacing helps reduce global namespace pollution and the potential for name conflicts when scoped enumerations are used in the global scope.
To access a scoped enumerator, we do so just as if it was in a namespace having the same name as the scoped enumeration:
#include <iostream>
int main()
{
enum class Difficulty // "enum class" defines this as a scoped enum rather than an unscoped enum
{
easy, // easy is considered part of Difficulty's scope region
hard,
};
std::cout << easy << '\n'; // compile error: easy not defined in this scope region
std::cout << Difficulty::easy << '\n'; // compile error: std::cout doesn't know how to print this (will not implicitly convert to int)
Difficulty gameLevel{ Difficulty::hard }; // okay
return 0;
}
Because scoped enumerations offer their own implicit namespacing for enumerators, there's no need to put scoped enumerations inside another scope region (such as a namespace), unless there's some other compelling reason to do so, as it would be redundant.
Scoped enumerations don't implicitly convert to integers
Unlike non-scoped enumerators, scoped enumerators won't implicitly convert to integers. In most cases, this is a good thing because it rarely makes sense to do so, and it helps prevent semantic errors, such as comparing enumerators from different enumerations, or expressions such as easy + 10.
Note that you can still compare enumerators from within the same scoped enumeration (since they are of the same type):
#include <iostream>
int main()
{
enum class Difficulty
{
easy,
hard,
};
Difficulty gameLevel{ Difficulty::easy };
if (gameLevel == Difficulty::easy) // this Difficulty to Difficulty comparison is okay
std::cout << "The game is on easy mode!\n";
else if (gameLevel == Difficulty::hard)
std::cout << "The game is on hard mode!\n";
return 0;
}
There are occasionally cases where it is useful to be able to treat a scoped enumerator as an integral value. In these cases, you can explicitly convert a scoped enumerator to an integer by using a static_cast. A better choice in C++23 is to use std::to_underlying() (defined in the
#include <iostream>
#include <utility> // for std::to_underlying() (C++23)
int main()
{
enum class Difficulty
{
easy,
hard,
};
Difficulty gameLevel{ Difficulty::hard };
std::cout << gameLevel << '\n'; // won't work, because there's no implicit conversion to int
std::cout << static_cast<int>(gameLevel) << '\n'; // explicit conversion to int, will print 1
std::cout << std::to_underlying(gameLevel) << '\n'; // convert to underlying type, will print 1 (C++23)
return 0;
}
Conversely, you can also static_cast an integer to a scoped enumerator, which can be useful when doing input from users:
#include <iostream>
int main()
{
enum class Direction
{
north, // assigned 0
south, // assigned 1
east, // assigned 2
west, // assigned 3
};
std::cout << "Enter a direction (0=north, 1=south, 2=east, 3=west): ";
int input{};
std::cin >> input; // input an integer
Direction heading{ static_cast<Direction>(input) }; // static_cast our integer to a Direction
return 0;
}
As of C++17, you can list initialize a scoped enumeration using an integral value without the static_cast (and unlike an unscoped enumeration, you don't need to specify a base):
// using enum class Direction from prior example
Direction heading{ 1 }; // okay
Favor scoped enumerations over unscoped enumerations unless there's a compelling reason to do otherwise.
Despite the benefits that scoped enumerations offer, unscoped enumerations are still commonly used in C++ because there are situations where we desire the implicit conversion to int (doing lots of static_casting gets annoying) and we don't need the extra namespacing.
Scoped enumerations are great, but the lack of implicit conversion to integers can sometimes be a pain point. If we need to convert a scoped enumeration to integers often (e.g. cases where we want to use scoped enumerators as array indices), having to use static_cast every time we want a conversion can clutter our code significantly.
If you find yourself in the situation where it would be useful to make conversion of scoped enumerators to integers easier, a useful hack is to overload the unary operator+ to perform this conversion:
#include <iostream>
#include <type_traits> // for std::underlying_type_t
enum class MenuItem
{
burger, // 0
pizza, // 1
salad, // 2
pasta, // 3
dessert, // 4
beverage, // 5
maxItems,
};
// Overload the unary + operator to convert an enum to the underlying type
// adapted from https://stackoverflow.com/a/42198760, thanks to Pixelchemist for the idea
// In C++23, you can #include <utility> and return std::to_underlying(item) instead
template <typename T>
constexpr auto operator+(T item) noexcept
{
return static_cast<std::underlying_type_t<T>>(item);
}
int main()
{
std::cout << +MenuItem::pasta << '\n'; // convert MenuItem::pasta to an integer using unary operator+
return 0;
}
This prints:
3
This method prevents unintended implicit conversions to an integral type, but provides a convenient way to explicitly request such conversions as needed.
using enum statements C++20
Introduced in C++20, a using enum statement imports all of the enumerators from an enum into the current scope. When used with an enum class type, this allows us to access the enum class enumerators without having to prefix each with the name of the enum class.
This can be useful in cases where we would otherwise have many identical, repeated prefixes, such as within a switch statement:
#include <iostream>
#include <string_view>
enum class Difficulty
{
easy,
medium,
hard,
};
constexpr std::string_view getDifficultyName(Difficulty difficulty)
{
using enum Difficulty; // bring all Difficulty enumerators into current scope (C++20)
// We can now access the enumerators of Difficulty without using a Difficulty:: prefix
switch (difficulty)
{
case easy: return "easy"; // note: easy instead of Difficulty::easy
case medium: return "medium";
case hard: return "hard";
default: return "???";
}
}
int main()
{
Difficulty gameLevel{ Difficulty::hard };
std::cout << "Your game difficulty is " << getDifficultyName(gameLevel) << '\n';
return 0;
}
In the above example, Difficulty is an enum class, so we normally would access the enumerators using a fully qualified name (e.g. Difficulty::hard). However, within function getDifficultyName(), we've added the statement using enum Difficulty;, which allows us to access those enumerators without the Difficulty:: prefix.
This saves us from having multiple, redundant, obvious prefixes inside the switch statement.
Summary
Scoped enumerations: Declared using enum class instead of enum, they provide type safety and namespace protection. Enumerators are scoped within the enumeration itself (accessed via Difficulty::hard rather than just hard), preventing namespace pollution and name conflicts.
Type safety benefits: Scoped enumerations won't implicitly convert to integers, preventing semantic errors like comparing enumerators from different enumeration types or performing arithmetic operations that don't make logical sense. This makes code more robust and catches bugs at compile time.
Explicit conversions: When integer conversion is necessary, use static_cast<int>(enumerator) or the C++23 std::to_underlying(enumerator) function. Conversely, integers can be cast to scoped enumerators using static_cast<EnumType>(integer). C++17 allows direct list initialization from integers without casting.
Unary operator+ hack: For code requiring frequent conversions to integers (such as array indexing), overloading operator+ provides a concise syntax: +MenuItem::pasta instead of static_cast<int>(MenuItem::pasta). This prevents unintended implicit conversions while offering convenient explicit conversion.
using enum statements (C++20): Import all enumerators from a scoped enumeration into the current scope using using enum EnumName;. This is particularly useful within switch statements, allowing case easy: instead of case Difficulty::easy: while maintaining the benefits of scoped enumerations elsewhere.
When to prefer scoped enumerations: Use scoped enumerations by default for their type safety and namespace protection. Unscoped enumerations remain useful when implicit integer conversion is desirable and namespace pollution isn't a concern, but this is the less common case.
Scoped enumerations represent modern C++ best practices for enumeration types, providing compile-time safety while maintaining flexibility through explicit conversions when needed.
Type-Safe Enumerations with enum class - Quiz
Test your understanding of the lesson.
Practice Exercises
Scoped Enumerations (enum class)
Create type-safe enumerations using enum class. Practice defining, using, and converting scoped enumerators.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!