Classy Enums

Back in another era, when I worked for UbiSoft, my then-boss Alain, started a wonderful initiative – the Technical Meeting. Every Friday afternoon, one of us should present a technical argument to the whole team. A good number of Technical Meetings were held, but when the project entered some frantic period. I have fond memories of these meetings.

So I was very happy when I heard that a similar initiative was going to happen at Schindler – the biweekly Technical Session. The idea is very similar – every two weeks one of the colleagues volunteers to make a short technical presentation and give it to the team. Topics are diverse, mainly related to C++. The goal is to have compact presentations limited to 10-15 minutes, ideally including some hands-on parts.

I volunteer for the second topic, which in turn I’ll present here.

The Many Problems of C enum

C enum, also known in C++ as unscoped enum, has a number of problems. The scoped enum proposal can give a detailed presentation of such problems.

Notably C enums:

  • lack of scope – all the labels are injected in the global namespace
  • are implicitly converted into ints, defeating type checking
  • pick the underlying type according to a fixed set of rules

Lack of scope

Since the global namespace is, well… global, you want to retain strict control of what goes to avoid polluting everywhere with global symbols. The problem is more evident for library providers. Two distinct libraries may inadvertently use the same symbol in two distinct enums causing incompatibility.

Implicit Conversion to int

Although it is true that two different unscoped enums can’t be assigned or implicitly converted into each other, the implicit conversion to int weakens the type safety. Consider the following example

enum Color { C_RED, C_BLUE, C_YELLOW };
enum Alert { A_GREEN, A_RED }

Alert a = A_RED;

bool armWeapons = (a >= C_YELLOW);

In this example comparing labels from two different enums is allowed because of implicit conversion.

Underlying type

C enums chose the underlying type according to a set of fixed rules based on the minimum and maximum values of the labels. More specifically, considering all the values assigned, either implicitly or explicitly to the enumeration items –

  • If every value fits into a signed int, then the underlying type is a signed int, otherwise
  • If every value fits in an unsigned int, then the underlying type is an unsigned int, otherwise
  • the underlying type is implementation-defined or the enum is ill-formed.

Although this set of rules may seem reasonable at first, it restricts the field of application of enums. If you need to rely on struct memory layout you can’t use enums, also converting from and to the wrong integer may yield the wrong result. And if the underlying type is signed, then bit-oriented operations and over/underflow are Undefined Behavior.

Scoped Enum

Scoped enums have been introduced with C++11 in order to address unscoped enums’ shortcomings.

You declare a scoped enum adding the “class” (or “struct”) keyword to the declaration

enum class Color {
  Red, Blue, Yellow
};
enum class Empty {};

Optionally you can explicitly set the underlying type by adding a colon and the required type –

Scoped Enum with Underlying Type Declarations

enum class Protocol : uint8_t {
   Reserved = 0,
   IPv4 = 4,
   StreamIpDatagram = 5,
   IPv6 = 6,
   ...
};

No longer are enums implicitly converted to integers nor integer operators can be applied to enums –

void f( int n );
enum class Color { Red, Green, Blue };
Color c = Color::Red;
c++; // error, can't do math
if( Color::Red < Color::Blue ) {} // error, no ordering
f( c ); // error, doesn't implicitly convert to int

Leveraging Scoped Enums for Compile Time Checks

Beyond addressing the deficiencies of C enumerations, scoped enums could also provide some benefits when applied in place of plain integers. Consider the following case.

You have an external memory to deal with, let’s say a flash chip. A library allows you to operate on this memory providing utility functions, such as reading, writing, and copying.

The address space of the external memory is distinct from the address space of the CPU and addresses are represented, on the CPU, via unsigned integers. This makes perfect sense because they are not C++ pointers since they do not point in the CPU address space.

In this scenario, there is a function that copies some bytes from one address to another –

using Address = uint_32;
void ExtMemCopy( Address source, Address dest, size_t size );

(I’m pretty sure that if you are familiar with firmware libraries from popular hardware vendors you have already seen functions like this).

When it comes to calling ExtMemCopy is too easy to swap source and dest, also because there is no standard rule for ordering these arguments (e.g. std::memcpy has destination first and then source, while std::copy wants first source and then the destination). Regardless of whether the right parameter is given to the right argument or not, the code is always valid, the compiler won’t complain and the error could be detected only via unit test (or worst at run time).

Enum class provides the tool for enabling compile-time checks preventing this whole class of errors. Let’s see how –

enum class SourceAddress : uint32_t {};
enum class DestAddress : uint32_t {};

void ExtMemCopy( SourceAddress source, DestAddress dest, size_t size );
// ...

auto a0 = SourceAddress( ... );
uint32_t a1 = ...

ExtMemCopy( a0, DestAddress( a1 ), SomeSize );

We define two refined types – SourceAddress and DestAddress. They are enum classes, with no declared items. This is valid and fine since we need just a dedicated type based on a given type (uint32_t in this case).

Function parameters now employ these types. If you attempt to call ExtMemCopy with plain numbers you’ll get a compilation error. Plus if you swap source and dest you will get also a compilation error since those are different types with no implicit conversion.

It is true that now you need a cast to the required type and that you can still miscast, but provided you are using meaningful and descriptive names for those types the chances are really low.

A second case worth noting is about boolean arguments. Consider the following code:

void replyGizmoState(uint32_t requestId, bool enabled, bool activated)
{ … }

Although the declaration could be clear enough, the call place could pose some effort in readability –

replyGizmoState( reqId, true, false );

Here the true and false arguments give no clue to the reader about what they mean. And without reading the documentation of replyGizmoState could be close to impossible to figure out what the reply is.

Enum class to the rescue! Consider rewriting the code above as –

enum class GizmoEnable : bool{ Disabled, Enabled };
enum class GizmoActivation : bool { Inactive, Active };

void replyGizmoState(uint32_t requestId, GizmoEnable enabled, GizmoActivation activated)
{
   ... 
}

...

// call point -
replyGizmoState( reqId, GizmoEnable::Enabled, GizmoActivation::Inactive );

Now there is no possible error and the readability has increased a lot. If you read my past posts, you may have a deja-vue feeling.

Some Guidelines

How to get the best from scoped enums, without stumbling into any problems? Indeed enum class is a very well thought device, with good defaults and clear syntax. It could be sometimes a bit verbose to write but usually is very easy and clear to read.

You can dump the unscoped enums altogether, but for the very specific points where you need to be backward compatible.

There is a general consensus for writing enumeration items using UpperCamelCase, I guess this is because the community wants to limit the use of the DRAGON_CASE to macros and preprocessor stuff. This way DRAGON_CASE marks places where you need to be careful because of the dreaded preprocessor.

Besides of traditional enumeration uses, you should strive to employ an enum class whenever possible to enforce stricter type checking on integral types. Compile time type checking is an incredibly powerful tool to prevent errors.

Conclusions

Legacy enumerations from C suffer a number of critical problems that weaken type checking and limit their application. C++ enum class, also known as scoped enum, fixes the flaws in its ancestor. By leveraging the characteristics of scoped enums it is possible to define tools for catching errors at compile time.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.