Blog / September 22, 2023 / 7 mins read / By Suneet Agrawal

Enum vs Enum Class in C++

In C++, enumerations (enums) are a powerful tool for creating named sets of integer constants. They help improve code readability and maintainability by providing meaningful names to numeric values. However, C++ offers two ways to define enums: the traditional enum and the more modern enum class. In this blog, we’ll explore the differences between these two options and when to use each with the help of examples.

Traditional enum

The traditional enum in C++ allows you to define a set of named integer constants without restricting their underlying type. This means the values are not encapsulated within a specific scope, leading to potential namespace clashes.

#include <iostream>

enum Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Red;
    // ...
    return 0;
}

In the example above, Red, Green, and Blue are in the same scope as the surrounding code, which can lead to naming conflicts if similar names exist elsewhere in your program.

enum class

In contrast, C++11 introduced the enum class (also known as a scoped enumeration or strong enum) to address the issues of traditional enums. enum class provides stronger type safety by encapsulating the values within their own scope. This makes it less error-prone and more self-contained.

#include <iostream>

enum class Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Color::Red;
    // ...
    return 0;
}

In this version, Color::Red, Color::Green, and Color::Blue are scoped within the Color enum class, reducing the chances of naming conflicts.

Differences Between enum and enum class

While both traditional enum and enum class serve the purpose of defining named sets of integer constants, they differ in several important ways. Understanding these differences is crucial for making an informed choice when deciding which one to use in your code:

Scoping:

enum: Enumerators declared using the traditional enum are in the same scope as the surrounding code. This can potentially lead to naming conflicts if similar names exist elsewhere in your program.

#include <iostream>

enum Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Red; // Enum value is in the same scope.
    int Green = 42;     // Potential naming conflict.
    
    std::cout << myColor << std::endl; // Prints 0 (Red)
    return 0;
}

enum class: Enumerators declared within an enum class are scoped within the enum class itself. This encapsulation prevents naming conflicts, as the enumerators are not in the global scope.

#include <iostream>

enum class Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Color::Red; // Enum value is scoped.
    int Green = 42;             // No naming conflict.
    
    std::cout << static_cast<int>(myColor) << std::endl; // Prints 0 (Red)
    return 0;
}
Type Safety:

enum: Traditional enums do not provide strong type safety. They allow implicit type conversions between the enum type and integral types, which can lead to unexpected behavior.

#include <iostream>

enum Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Red;
    int myInt = 1;

    if (myColor == myInt) {
        std::cout << "Colors match!" << std::endl; // Compiles, but unexpected behavior.
    }

    return 0;
}

enum class: Enum classes offer strong type safety. They do not allow implicit type conversions, helping to prevent unintended assignments or comparisons between different enum types.

#include <iostream>

enum class Color {
    Red,
    Green,
    Blue
};

int main() {
    Color myColor = Color::Red;
    int myInt = 1;

    if (myColor == myInt) {
        std::cout << "Colors match!" << std::endl; // Error: no match for 'operator=='
    }

    return 0;
}
Underlying Type:

enum: By default, traditional enums have an underlying integral type, usually int. You can explicitly specify the underlying type, but it’s not mandatory.

#include <iostream>

enum Color {
    Red,
    Green,
    Blue
};

int main() {
    std::cout << sizeof(Color) << std::endl; // Size is implementation-dependent.
    return 0;
}

enum class: Enum classes require you to specify the underlying type explicitly. This promotes clarity and consistency in your code.

#include <iostream>

enum class Color : char {
    Red,
    Green,
    Blue
};

int main() {
    std::cout << sizeof(Color) << std::endl; // Size is guaranteed to be 1 byte (char).
    return 0;
}
Enumeration Values:

enum: Enumeration values are directly accessible without any scope qualification, potentially leading to ambiguous or conflicting names.

#include <iostream>

enum Day {
    Sunday,     // 0
    Monday,     // 1
    Tuesday,    // 2
    Wednesday,  // 3
    Thursday,   // 4
    Friday,     // 5
    Saturday    // 6
};

int main() {
    Day today = Monday;
    int dayValue = today; // Enum value directly used as an integer.
    
    std::cout << dayValue << std::endl; // Prints 1
    return 0;
}

enum class: Enumeration values require qualification with the enum class name, making it clear and unambiguous when you refer to them.

#include <iostream>

enum class Day {
    Sunday,     // Day::Sunday
    Monday,     // Day::Monday
    Tuesday,    // Day::Tuesday
    Wednesday,  // Day::Wednesday
    Thursday,   // Day::Thursday
    Friday,     // Day::Friday
    Saturday    // Day::Saturday
};

int main() {
    Day today = Day::Monday;
    // int dayValue = today; // Error: no viable conversion from 'Day' to 'int'.
    
    // std::cout << dayValue << std::endl; // Compilation error
    return 0;
}
Enum Size:

enum: The size of a traditional enum is implementation-dependent, as it is determined by the underlying integral type.

#include <iostream>

enum Planet {
    Mercury,  // 0
    Venus,    // 1
    Earth,    // 2
    Mars      // 3
};

int main() {
    std::cout << sizeof(Planet) << std::endl; // Size is implementation-dependent.
    return 0;
}

enum class: The size of an enum class is guaranteed to be the same as the size of its underlying integral type, ensuring consistency across platforms.

#include <iostream>

enum class Month : char {
    January,   // Month::January
    February,  // Month::February
    March,     // Month::March
    April      // Month::April
};

int main() {
    std::cout << sizeof(Month) << std::endl; // Size is guaranteed to be 1 byte (char).
    return 0;
}
Default Values:

enum: Traditional enums can implicitly convert to integers, and uninitialized enum variables typically have an undefined value.

#include <iostream>

enum Mood {
    Happy,    // 0
    Sad,      // 1
    Angry     // 2
};

int main() {
    Mood personMood; // Uninitialized enum variable.
    
    std::cout << personMood << std::endl; // May contain an undefined value.
    return 0;
}

enum class: Enum class variables are not implicitly convertible to integers, and uninitialized enum class variables have a well-defined default value (enum_class_type::first_enumerator).

#include <iostream>

enum class State {
    On,  // State::On
    Off  // State::Off
};

int main() {
    State deviceState; // Uninitialized enum class variable.
    
    // std::cout << static_cast<int>(deviceState) << std::endl; // Prints 0 (State::On is the default)
    return 0;
}
Enum Constants:

enum: Enum constants can be defined with any valid integer value, including duplicates and values outside the declared range.

#include <iostream>

enum Direction {
    North = 1,  // Assigned value 1
    South = 2,  // Assigned value 2
    East = 3,   // Assigned value 3
    West = 1    // Duplicate value (allowed)
};

int main() {
    std::cout << East << std::endl; // Prints 1 (duplicate values allowed)
    return 0;
}

enum class: Enum class constants are strongly enforced to be unique within the scope and adhere to the underlying type’s range.

#include <iostream>

enum class Language : short {
    English = 1,  // Assigned value 1
    Spanish = 2,  // Assigned value 2
    French = 3,   // Assigned value 3
    German = 1    // Error: duplicate enumerator 'Language::German'
};

int main() {
    // std::cout << static_cast<short>(Language::German) << std::endl; // Compilation error
    return 0;
}

Benefits of enum class

Strong Typing: Enum classes offer stronger typing, ensuring that you cannot accidentally mix different enum types or assign arbitrary integer values to them.

Scoped Names: Enum classes encapsulate their values within a specific scope, preventing naming clashes with other parts of your code.

Improved Code Readability: The use of the scope operator (::) makes it clear which enum the value belongs to, enhancing code readability.

Type Safety: Enum classes provide better type safety by restricting implicit type conversions, making your code less error-prone.

When to Choose Each

Use Traditional enum When…
  • You need backward compatibility with older C++ standards.
  • You want the enumeration values to be in the same scope as the surrounding code.
  • You don’t need strong type safety or scoping for the enumeration.
Use enum class When…
  • You are working with C++11 or later versions.
  • You want to avoid naming conflicts and ensure better code organization.
  • You need stronger type safety to prevent unintended assignments or comparisons between different enum types.
  • You value code readability and want to make your intentions explicit.

Conclusion

Choosing between enum and enum class in C++ depends on your specific requirements and the version of C++ you are using. While traditional enums are still relevant in some scenarios, enum class offers better code organization, type safety, and scoping, making it the preferred choice in modern C++ development. Always consider the context and choose the enum type that best suits your project’s needs to write clean and maintainable code.

Comments