cpp

Enumerations in C++

pauljlucas

Paul J. Lucas

Posted on October 22, 2023

Enumerations in C++

Introduction

C++ inherited enumerations from C, warts and all. Everything about enumerations in C also applies in enumerations C++ (so if you haven’t read that article, you should). The problems with enumerations in C are:

  • Constants are not scoped; instead, they are “injected” into the surrounding scope. Sometimes, this causes name collisions.

  • Values implicitly convert to their underlying integral value and vice versa. While sometimes convenient, this can lead to bugs silently creeping in.

  • Enumerations can not be forward-declared.

C++11 extended enumerations to fix all of these problems.

enum class

So as to remain backwards-compatible with C, enumerations weren’t touched. Instead, C++11 added enumeration classes that fix all of the issues with C-style enumerations:

enum class color {  // Note "class" keyword.
  BLACK,
  WHITE,
  BLUE,
  GREEN,
  RED,
};
Enter fullscreen mode Exit fullscreen mode

Incidentally, enum struct can also be used, but there’s no difference.

Constant Scoping

Enumeration classes immediately fix the scoping problem in that enumeration constants are not “injected” into the surrounding scope. Instead, they’re referred to via the :: operator:

color c = color::BLACK;
Enter fullscreen mode Exit fullscreen mode

For C-style enumerations, :: can also be used even though it isn’t necessary.

Constant Conversion

Unlike C-style enumerations, enumeration class constants do not implicitly convert to their underlying integral values:

int n = color::RED;                      // error
Enter fullscreen mode Exit fullscreen mode

Instead, an explicit cast is required:

int n = static_cast<int>( color::RED );  // OK
Enter fullscreen mode Exit fullscreen mode

Bit Flag Values

If enumeration class constants do not implicitly convert to their underlying integral values, then the bitwise operators don’t work:

enum class c_int_fmt {
  NONE     = 0,
  SHORT    = 1 << 0,
  INT      = 1 << 1,
  LONG     = 1 << 2,
  UNSIGNED = 1 << 3,
  CONST    = 1 << 4,
  STATIC   = 1 << 5,
};

c_int_fmt f = c_int_fmt::UNSIGNED | c_int_fmt::INT;  // error
Enter fullscreen mode Exit fullscreen mode

However, you can overload the bitwise operators:

constexpr c_int_fmt operator|( c_int_fmt lhs, c_int_fmt rhs ) {
  using U = std::underlying_type_t<c_int_fmt>;
  return static_cast<c_int_fmt>( static_cast<U>(lhs) | static_cast<U>(rhs) );
}
Enter fullscreen mode Exit fullscreen mode

You can overload the other bitwise operators &, ^, ~, |=, &=, and ^= similarly. However, if you agree that it’s rather tedious to overload seven operators for every enumeration class you’re using for bit flag values, you define a macro like:

#define ENABLE_BITWISE_OPERATORS_FOR_ENUM(E)                            \
  static_assert( std::is_enum_v<E>, "enumeration type required" );      \
  constexpr E operator|( E lhs, E rhs ) {                               \
    using U = std::underlying_type_t<E>;                                \
    return static_cast<E>( static_cast<U>(lhs) | static_cast<U>(rhs) ); \
  }                                                                     \
  constexpr E operator&( E lhs, E rhs ) {                               \
    using U = std::underlying_type_t<E>;                                \
    return static_cast<E>( static_cast<U>(lhs) & static_cast<U>(rhs) ); \
  }                                                                     \
  constexpr E operator^( E lhs, E rhs ) {                               \
    using U = std::underlying_type_t<E>;                                \
    return static_cast<E>( static_cast<U>(lhs) ^ static_cast<U>(rhs) ); \
  }                                                                     \
  constexpr E operator~( E e ) {                                        \
    using U = std::underlying_type_t<E>;                                \
    return static_cast<E>( ~static_cast<U>( e ) );                      \
  }                                                                     \
  constexpr E operator|=( E &lhs, E rhs ) {                             \
    return (lhs = lhs | rhs);                                           \
  }                                                                     \
  constexpr E operator&=( E &lhs, E rhs ) {                             \
    return (lhs = lhs & rhs);                                           \
  }                                                                     \
  constexpr E operator^=( E &lhs, E rhs ) {                             \
    return (lhs = lhs ^ rhs);                                           \
  }                                                                     \
  using type_to_eat_semicolon = int
Enter fullscreen mode Exit fullscreen mode

And then use it like:

ENABLE_BITWISE_OPERATORS_FOR_ENUM( c_int_fmt );
Enter fullscreen mode Exit fullscreen mode

The type_to_eat_semicolon is a common trick used when you want to use ; after a use of a macro, but the macro’s definition ends with something that doesn’t allow a ; to follow it, in this case the closing } of a function definition. The trick works because it’s legal to alias a type via using (or typedef) multiple times so long as the aliased type is the same.

Forward Declaration

Enumerations can be forward-declared, but only when the underlying type is specified:

enum class color : uint8_t;
Enter fullscreen mode Exit fullscreen mode

Forward declaration is also allowed for C-style enumerations so long as the underlying type is specified.

Older C++ Code

In pre-C++11 code you may encounter, there was a trick to making C-style enumerations not inject their constants into the global scope:

namespace color {  // Pre-C++11 trick.
  enum type {
    BLACK,
    WHITE,
    BLUE,
    GREEN,
    RED,
  };
}
Enter fullscreen mode Exit fullscreen mode

That is, wrap the enumeration inside a namespace with the name you would have given the enumeration and always name the enumeration type:

color::type c = color::RED;
Enter fullscreen mode Exit fullscreen mode

This trick is no longer necessary. However, it’s still perfectly fine and useful to be able to put enumerations inside either classes or namespaces for the same reasons you’d put anything inside classes or namespaces.

Nested Enumerations

If you have an enumeration class nested inside either a class or namespace like:

namespace vt100 {
  enum class color {
    BLACK,
    WHITE,
    BLUE,
    GREEN,
    RED,
  };
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Then when using it:

vt100::color c = vt100::color::RED;  // Verbose.
Enter fullscreen mode Exit fullscreen mode

It gets a bit verbose to have to always specify color when though you’ve already specified vt100. Starting in C++20, you can “import” enumeration constant names into their enclosing scope:

namespace vt100 {
  enum class color {
    BLACK,
    WHITE,
    BLUE,
    GREEN,
    RED,
  };
  using enum color;  // Import constants into vt100 namespace.
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now you can instead do:

vt100::color c = vt100::RED;         // Better.
Enter fullscreen mode Exit fullscreen mode

Conclusion

All of the best practices that apply to enumerations in C also apply to enumerations in C++.

Additionally, in new C++ code, enumeration classes should be used exclusively, especially for those declared in the global scope so as not to “pollute” the global namespace with all their constant names. Use C-style enumerations only when compatibility with C is required.

💖 💪 🙅 🚩
pauljlucas
Paul J. Lucas

Posted on October 22, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related