C++ enum types pack a set of related constants in an intuitive and efficient user-defined type. Can you ask for more? With two new C++11 enhancements, namely scoped enums and based enums, the answer is “yes.” Find out all about the recent facelift that C++11 enums underwent and learn how to refactor your code to benefit from the new enum features – without sacrificing performance or backward compatibility.
Enums are one of my favorite C++ features. They exemplify the notion of an efficient user-defined type without the heavy machinery of virtual functions, constructors, etc. (Compare C++ enums to other programming languages that still insist on using a full-blown class instead, and you’ll see what I mean.)
Yet, traditional enum types aren’t flawless. Throughout the past three decades, some of their limitations have irritated programmers and compilers alike. Three of the most frequently cited drawbacks of traditional enums are:
- Enumerator names are visible from their enum’s enclosing scope.
- You cannot override the default underlying type of an enum programmatically.
- Enumerators convert to int automatically, no questions asked.
C++11 addresses these issues with revamped enumerations that give you tighter control over the scope, size, and implicit conversions of enum types. Let’s look at these new features more closely, and examine how they can improve both our code quality and frustration level.
A Matter of Scope
Programmers often discover the hard way that enumerators in the same scope must all be distinct from each other and from other variable names. Otherwise, they might clash, as the following example shows:
enum Color {
Bronze,
Silver,
Gold
};
enum Bullion
{
Silver, //conflicts with Color’s Silver
Gold, //conflicts with Color’s Gold Platinum
};
The enumerators Silver and Gold belong to different enum types and have different values. However, they clash because they’re visible from their enclosing scope (unlike, say, members of a class). The common workaround, namely appending the enum name to every enumerator, evokes memories from the pre-namespace era:
enum Color
{
BronzeColor,
SilverColor,
GoldColor
};
enum Bullion
{
SilverBullion,
GoldBullion,
PlatinumBullion
};
However, cumbersome names aren’t to everyone’s taste. Besides, name conflicts could still occur if you use third-party code.
Declaring enum types in namespaces offers only a partial solution to the problem. A typical project often declares all of its constants and enum types under the same namespace; and frankly, who uses namespaces these days anyway?
Enter Scoped Enums
C++11 solves the scoping problem with a new category called scoped enums. A scoped enum looks exactly as a traditional enum except that the keyword class (or struct – the two keywords are interchangeable in this context) appears between the keyword enum and the enum name, as shown in the following example:
enum class Color //C++11 scoped enum
{
Bronze,
Silver,
Gold
};
enum struct Bullion //C++11 scoped enum
{
Silver, //doesn’t conflict with Color’s Silver
Gold, //ditto
Platinum
};
Enumerators of a scoped enum require a qualified name when referred to from an enclosing scope:
Color col1=Bronze; //error, Bronze not in scope
Color col2=Color::Bronze; //OK
if ((col2==Color::Silver) || (col2==Color::Gold))//OK
//…
Strong Typing
Traditional enumerators aren’t strongly typed. They automatically convert to int:
int cointype= BronzeColor; //OK, unscoped enum
As opposed to their unscoped counterparts, scoped enums are strongly-typed. Implicit conversions to int (or any other integral types) are not permitted. To convert a scoped enumerator to int, you have to use an explicit cast:
int carcolor=Color::Silver; //error, implicit conversion to int not allowed
int carcolor=static_cast<int>(Color::Silver); //OK
One Size Doesn’t Fit All
All enum types are implemented as a built-in integral type known as the underlying type. In C, the underlying type is always int. In C++03, the underlying type is implementation-defined. An implementation is free to choose an arbitrary integral type (char, short, int, long, and their unsigned counterparts) as an enum’s underlying type. Furthermore, a C++ implementation is allowed to assign a different underlying type to different enum types. The only requirements in the C++03 standard are that the underlying type is an integral type that can represent all the enumerator values defined in the enumeration, and that the underlying type is not larger than int, unless the value of an enumerator cannot fit in an int or unsigned int.
Thus, a typical C++03 implementation may assign different underlying types to the following enum types:
//C++03
enum Bool {False, True}; // fits into char, signed char, unsigned char
enum RecSize
{DefaultSize=1000000, LargeSize=2000000}; //underlying type is int
Of course, an implementation may adhere to int as the sole underlying type regardless of the values of the enumerators. From my experience, that’s what the majority of C++03 compilers in fact do.
The under-specification of an enum’s underlying type in C++03 can be a problem if you need platform-independent sizes, e.g., when sending data across an HTTP connection, or when the data has to be stored compactly. In C++03, there’s really nothing much that you can do to override the default underlying type – at least not in a standardized manner.
C++11, however, lets you specify the underlying type of an enum explicitly by using an enum base. An enum base declaration looks syntactically similar to a base class in a derived class declaration:
enum Bool: char {False, True}; //C++11 based enum
In the example above, the programmer specified char as the underlying type of Bool. Consequently, every enumerator of Bool occupies only one byte of memory. If the value of an enumerator cannot be represented by the underlying type, a compilation error occurs.
Selecting char as the underlying type of an enum type ensures among the rest that enumerators can be transmitted via HTTP without the onerous little-endian versus big endian byte reordering. Additionally, the compact size of one byte (instead of four) per enumerator implies that a large number of enum values can be stored efficiently in a smartphone’s memory or a data file.
Of course, this doesn’t mean that you should rush to specify char as the underlying type of your enum types – quite the contrary. As a rule, let your compiler assign the default underlying type unless you have a good reason to override it. Usually, the default underlying type is also the most efficient datatype for the target hardware.
Combining Features
Based enums aren’t scoped by default. They simply have a fixed, user-specified underlying type. If you want the benefits of both scoped enums and based enums, combine the two features, like this:
enum class Bool: char {False, True}; //C++11 scoped and based enum
int x=sizeof (Bool); //x=1
int y =static_cast<int> (Bool::False); //y=0
++y;
Of course, traditional enums are still available in C++11 with the same semantics and scoping rules as before. Therefore, legacy code will not break when you upgrade your compiler. For example, a traditional enumerator will still convert to int implicitly even in C++11:
enum Direction (Up, Down};
int dir=Down; //OK in C++11 as well
Recall that you may use qualified enumerator names even with traditional enums (with scoped enums, you’re obliged to use qualified names exclusively):
int dir=Direction::Down; //OK, unscoped enum
However, it’s advisable to go through existing C++ code and refactor it, possibly replacing legacy enums with scoped enums; this shouldn’t affect their underlying type. Likewise, if you need a fixed, platform-independent underlying type, add an enum base to your enum declarations.
In Conclusion
C++11 offers two new categories of enum types: scoped enums and based enums. Enumerators of a scoped enum require a qualified name such as enumname::enumerator when you refer to them from an enclosing scope. In addition, scoped enums are strongly-typed, so you must use an explicit cast to convert them to int if necessary. A based enum lets you specify its underlying type programmatically. With respect to compiler support, GCC 4.4, Intel’s C++ 12, MSVC 11, IBM’s XLC++ 12.1, Clang 2.9, Embarcadero C++ Builder, and others support the new C++11 enum features.
Danny Kalev is a certified system analyst and software engineer specializing in C++. Kalev has written several C++ textbooks and contributes C++ content regularly on various software developers' sites. He was a member of the C++ standards committee and has a master's degree in general linguistics.
See also:
[dfads params='groups=932&limit=1&orderby=random']
[dfads params='groups=937&limit=1&orderby=random']