C++ Enums, short for enumerations, are a powerful and versatile feature of the C++ programming language. Enums provide a way to create named constant values representing a set of related integral constants. While they may seem simple on the surface, C++ Enums offer a range of functionalities that can significantly enhance code clarity, maintainability, and extensibility. In this article, we will explore C++ Enums, their types, properties, and utility functions introduced in different C++ standards.
Enumerations in C++
Enumerations, often referred to as enums, are user-defined data types in C++ that consist of a set of named integral constants. Enums help improve code readability by allowing developers to use meaningful names instead of hard-coded numerical values. Let’s start by exploring how to declare and use enums in C++.
#include <iostream>
// Declaration of a simple enum named Color
enum Color {
RED,
GREEN,
BLUE
};
int main() {
// Using the Color enum to declare a variable
Color color = RED;
// Switch statement to handle different enum values
switch (color) {
case RED:
std::cout << "The color is red." << std::endl;
break;
case GREEN:
std::cout << "The color is green." << std::endl;
break;
case BLUE:
std::cout << "The color is blue." << std::endl;
break;
}
return 0;
}
In this example, we’ve defined a simple enum named Color with three enumerators: RED, GREEN, and BLUE. We then declare a variable color of type Color and use a switch statement to handle different color cases.
Enumerators and Underlying Types:
By default, C++ Enums are based on integers, and each enumerator is assigned an integer value starting from 0. However, you can explicitly set the values for the enumerators:
#include <iostream>
enum Weekday {
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
};
int main() {
Weekday day = Saturday;
std::cout << day << std::endl; // 6
return 0;
}
Enums also support specifying a different underlying integer type, providing more control over the range of values. By default, enums use int as their underlying type, but you can change it:
#include <iostream>
enum Month : unsigned char {
January = 'A',
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
};
int main() {
Month month = December;
std::cout << month << std::endl; // 'L'
return 0;
}
In this example, we’ve used unsigned char as the underlying type for the Month enum, conserving memory when storage efficiency is a concern.
Scoping Enums with Enum Classes
While traditional enums provide a way to create named constants, they also have a drawback: all enumerators are injected into the surrounding scope. This can lead to naming conflicts, especially in large codebases. C++11 introduced the concept of enum classes, which provide better scoping and type safety compared to traditional enums. Enum classes are declared using the enum class syntax, and the enumerators are scoped within the enum class:
#include <iostream>
// Scoped enum declaration
enum class Status {
Success,
Failure
};
// Function using scoped enum
void processStatus(Status status) {
if (status == Status::Success) {
std::cout << "Operation successful!" << std::endl;
} else {
std::cout << "Operation failed." << std::endl;
}
}
int main() {
// Using scoped enum values
Status result = Status::Success;
processStatus(result);
return 0;
}
In this example, the Status enum is now a scoped enum. To access its values, you use the enum class name as a qualifier (Status::Success). This helps prevent naming collisions and promotes better code organization.
Enumerations and Switch Statements
One of the most compelling use cases for enums is in switch statements. Enums provide a clear and concise way to handle multiple cases, improving code readability. Consider the following example:
#include <iostream>
// Enum declaration
enum class TrafficLight {
Red,
Green,
Yellow
};
// Function using enum in a switch statement
void signalMessage(TrafficLight color) {
switch (color) {
case TrafficLight::Red:
std::cout << "Stop!" << std::endl;
break;
case TrafficLight::Green:
std::cout << "Go!" << std::endl;
break;
case TrafficLight::Yellow:
std::cout << "Prepare to stop." << std::endl;
break;
}
}
int main() {
// Using enum in a switch statement
TrafficLight currentColor = TrafficLight::Green;
signalMessage(currentColor);
return 0;
}
Switch statements using enums are not only more readable but also safer. If a new enumerator is added to the TrafficLight enum, the compiler can catch potential omissions in switch statements, reducing the risk of bugs.
The is_enum Template
Introduced in C++11, the is_enum template is a valuable addition to the C++ type traits library. This template allows developers to determine whether a given type is an enumeration type. Let’s explore its usage:
#include <iostream>
#include <type_traits>
// Helper function to check if a type is an enum
template <typename T>
void checkIsEnum() {
if (std::is_enum<T>::value) {
std::cout << "The type is an enumeration." << std::endl;
} else {
std::cout << "The type is not an enumeration." << std::endl;
}
}
enum ExampleEnum {
OPTION1,
OPTION2
};
struct NotAnEnum {
int value;
};
int main() {
checkIsEnum<ExampleEnum>(); // Output: The type is an enumeration.
checkIsEnum<int>(); // Output: The type is not an enumeration.
checkIsEnum<NotAnEnum>(); // Output: The type is not an enumeration.
return 0;
}
In this example, the checkIsEnum function uses std::is_enum to check whether the provided type is an enumeration. It prints a corresponding message to the console based on the result. The output demonstrates the use of this template for both enumeration and non-enumeration types.
The is_scoped_enum Template (C++23)
In C++23, a new template named is_scoped_enum is introduced to determine whether a given type is a scoped enumeration type. Scoped enumerations, also known as strongly typed enums, were introduced in C++11 to address some of the limitations of traditional enums. Unlike traditional enums, scoped enums have their own scope, and their enumerators don’t leak into the surrounding scope.
#include <iostream>
#include <type_traits>
// Helper function to check if a type is a scoped enum
template <typename T>
void checkIsScopedEnum() {
if (std::is_scoped_enum<T>::value) {
std::cout << "The type is a scoped enumeration." << std::endl;
} else {
std::cout << "The type is not a scoped enumeration." << std::endl;
}
}
enum class ScopedEnum {
OPTION1,
OPTION2
};
enum TraditionalEnum {
OPTION_A,
OPTION_B
};
int main() {
checkIsScopedEnum<ScopedEnum>(); // Output: The type is a scoped enumeration.
checkIsScopedEnum<TraditionalEnum>(); // Output: The type is not a scoped enumeration.
return 0;
}
In this example, we define a scoped enumeration ScopedEnum and a traditional enumeration TraditionalEnum. The checkIsScopedEnum function utilizes std::is_scoped_enum to determine whether the provided type is a scoped enumeration. The output demonstrates the distinction between scoped and traditional enums.
The underlying_type Template
The underlying_type template, introduced in C++11, provides a way to obtain the underlying integer type for a given enumeration type. This is particularly useful when you need to work directly with the numeric values associated with enum constants.
#include <iostream>
#include <type_traits>
enum class Status : char {
OK,
ERROR
};
int main() {
// Using underlying_type to obtain the underlying type of the enum
using UnderlyingType = std::underlying_type<Status>::type;
UnderlyingType statusValue = static_cast<UnderlyingType>(Status::OK);
std::cout << "The underlying type of Status enum is char." << std::endl;
std::cout << "Numeric value of Status::OK: " << statusValue << std::endl;
return 0;
}
In this example, the Status enum is declared with an underlying type of char. The std::underlying_type template is then used to obtain the underlying type, and the numeric value of the Status::OK enumerator is printed to the console.
The to_underlying Function (C++23)
In C++23, the to_underlying function is introduced, providing a more convenient way to convert an enumeration to its underlying type. This function simplifies the process of obtaining the numeric value associated with an enum constant.
#include <iostream>
enum class Month {
JANUARY = 1,
FEBRUARY,
MARCH,
APRIL,
MAY,
JUNE,
JULY,
AUGUST,
SEPTEMBER,
OCTOBER,
NOVEMBER,
DECEMBER
};
int main() {
// Using to_underlying to obtain the underlying value of the enum constant
auto januaryValue = std::to_underlying(Month::JANUARY);
std::cout << "Underlying value of Month::JANUARY: " << januaryValue << std::endl;
return 0;
}
In this example, the to_underlying function is used to directly obtain the underlying value of the Month::JANUARY enumerator without the need for explicit casting. This simplifies the code and enhances readability.
Conclusion
C++ Enums are a powerful tool for creating more expressive and readable code by assigning meaningful names to constant values. With the introduction of features like is_enum, is_scoped_enum, underlying_type, and to_underlying, C++ provides developers with enhanced capabilities for working with enums. These features contribute to safer and more efficient code by enabling type checks, facilitating underlying type retrieval, and simplifying enum-to-integer conversions.
References: