Thursday, December 6, 2012

Strong enum: enum class

Strong enum: enum class
Strong enum: enum class

The C++11 standard expands the enum type by adding some class-like features to it. This new type is called enum class, also known as, strong enum or new enum. The enum class provides the following three new features:
  • Strong scope: enumerators are scoped within the enum type.
  • Strong type: no implicit conversion to integral type.
  • Ability to specify the underlying type.
Let us look at these features with examples, comparing them with the old C++ (C++03) enum.
Strong scope: enumerators are scoped withing the enum type
C++03 enumerators (also known as enum values) are scoped within the enclosing scope. The following code, which looks logically correct, creates a naming conflict and thus does not compile on C++03 http://ideone.com/XREQ9. The enums Fruit and Paint are enclosed within the inventory class scope, and hence their enumerators get enclosed in it too.
class Inventory {
public:
enum Fruit { APPLE, BANANA, GRAPE, ORANGE };
enum Paint { RED, BLUE, GREEN, ORANGE };
};
int main() {
}

compilation info
prog.cpp:4: error: declaration of 'ORANGE'
prog.cpp:3: error: conflicts with previous declaration 'Inventory::Fruit Inventory::ORANGE'

C++11 enum class gives the enum type its own scope, and places its enumerators in this scope. The following code example shows how an enum class is defined and used. [*1] http://ideone.com/cH3YI
#include<iostream>
using namespace std;
class Inventory {
public:
enum class Fruit { APPLE, BANANA, GRAPE, ORANGE };
enum class Paint { RED, BLUE, GREEN, ORANGE };
};
int main() {
Inventory::Fruit leFruit = Inventory::Fruit::ORANGE;
if(leFruit == Inventory::Fruit::ORANGE) {
cout << "Behold this, for this is an orange." << endl;
}
}

output:
Behold this, for this is an orange.

As an aside, in the above code, we could use another awesome C++11 feature, auto, which leaves it to the compiler to deduce the type of leFruit from the initializer. http://ideone.com/7Yvki
auto leFruit = Inventory::Fruit::ORANGE;

Strong type: No implicit conversion to integral types
C++03 allows an enum to implicitly convert to integral types. This can lead to subtle logic defects, as in the following code. http://ideone.com/FKI0d
#include <iostream>
using namespace std;
enum E { e1 = 127, e2 };
int main() {
char c = e2; // Undefined Behaviour, e2 (128) is out of valid range for char
cout << c << endl;
}

Moreover, an enum is meant to be a separate data type in letter and spirit, and it is absurd to think "an enum is an int". [*2] This implicit conversion also allows comparison between two different enums http://ideone.com/rkSnD.
#include <iostream>
using namespace std;
enum Fruit { APPLE, BANANA, GRAPE, ORANGE };
enum Vehicle { BICYCLE, CAR, BUS, TRUCK };
int main() {
int num1 = APPLE; // A Fruit is a number!
cout << "num1 is " << num1 << endl;
if(CAR == BANANA) { // A Vehicle is a Fruit!!
cout << "Cars drive me bananas!" << endl;
}
}

output:
num1 is 0
Cars drive me bananas!

C++11 enum class disallows implicit conversion of enum to int. http://ideone.com/E1YoX
#include <iostream>
using namespace std;
enum class Fruit { APPLE, BANANA, GRAPE, ORANGE };
enum class Vehicle { BICYCLE, CAR, BUS, TRUCK };
int main() {
int num1 = Fruit::APPLE;
if(Vehicle::CAR == Fruit::BANANA) {
cout << "Cars drive me bananas!" << endl;
}
}

compilation info
prog.cpp: In function 'int main()':
prog.cpp:8:23: error: cannot convert 'Fruit' to 'int' in initialization
prog.cpp:9:31: error: no match for 'operator==' in '(Vehicle)1 == (Fruit)1'
prog.cpp:9:31: note: candidates are: operator==(Fruit, Fruit) <built-in>
prog.cpp:9:31: note: operator==(Vehicle, Vehicle) <built-in>

For any special reason, if you still want to convert an enum to int, or an enum to another enum, you need to explicitly state your intentions using static_cast. http://ideone.com/20Qr8
#include <iostream>
using namespace std;
enum class Fruit { APPLE, BANANA, GRAPE, ORANGE };
enum class Vehicle { BICYCLE, CAR, BUS, TRUCK };
int main() {
int num1 = static_cast<int>(Fruit::APPLE);
cout << "num1 is " << num1 << endl;
if(static_cast<Fruit>(Vehicle::CAR) == Fruit::BANANA) {
cout << "Cars drive me bananas!" << endl;
}
}

output:
num1 is 0
Cars drive me bananas!

Ability to specify the underlying type
According to C++03, the underlying type of the enum is left implementation defined. The enumerators have to be declared with the enum declaration to let the compiler choose the appropriate type. C++11 enum class lets us explicitly specify the underlying type, using the following syntax.
enum class Fruit : char { APPLE, BANANA, GRAPE, ORANGE }; // underlying type is char
enum class Vehicle { BICYCLE, CAR, BUS, TRUCK }; // default underlying type is int

This provides two key benefits.
  • It permits forward declaration of the enum class.
  • It allows us to provide a guaranteed enumeration size across implementations.
Forward declaration of the enum class
Forward declaration of the enum class enables us to separate the declaration of the enum class from the declaration of its enumerators. In particular, this lets us put the enum class declaration in the header file, and the enumerators in the source file, which allows the enumerators to be changed without having to recompile the code which uses the enum. It can also speed up compilation for the code which uses the enum, because the compiler usually does not need the enumerators to compile it. The parent class may also define a private enum class type member for its own internal computations, without exposing the enumerators.
The below example illustrates these benefits of forward declaration. [*3] Notice that the enumerators and the methods can be changed in Inventory.cpp without the need to recompile main.cpp. Moreover, to compile main.cpp, the compiler does not need to know the enumerators of Fruit. It only needs to know the underlying type (which, in this case, is int, by default).
// header file Inventory.h
#ifndef Inventory_h
#define Inventory_h
class Inventory {
public:
enum class Fruit;
Fruit getFruit();
float getCost(Fruit leFruit);
};
#endif
// source file Inventory.cpp
#include "Inventory.h"
enum class Inventory::Fruit { APPLE, BANANA, GRAPE, ORANGE };
Inventory::Fruit Inventory::getFruit() { // the awesome 'auto' is useful here too, but let's skip it for now
return Fruit::APPLE;
}
float Inventory::getCost(Fruit leFruit) {
switch(leFruit) {
case Fruit::APPLE:
return 1.99;
break;
default: // other fruits are out of stock
return 0.0;
}
}

// main program
#include "Inventory.h"
#include <iostream>
using namespace std;
int main() {
Inventory leInventory;
Inventory::Fruit leFruit = leInventory.getFruit();
float leCost = leInventory.getCost(leFruit);
cout << "The fruit costs " << leCost << endl;
}

output:
The fruit costs 1.99

Guaranteed enumeration size
In some applications, it may be necessary that the underlying type have a fixed number of bits. The ISO standard mandates minimum bit widths for the built-in types, but leaves the exact bit widths as implementation defined. In such cases, the enum class declaration can specify its underlying type as one of the standard fixed bit width types from the standard library <cstdint>.
#include <cstdint>
enum class Opcode : std::uint16_t;

What happens to the old C++03 enum?
C++11 is fully backward compatible with the C++03 enum, and hence, C++03 can be used in C++11 code. In addition, C++11 applies the strong scoping to a C++03 enum. Thus, an enumerator of a C++03 is placed in the enum scope as well as in the scope enclosing the enum http://ideone.com/wV3G9.
#include <iostream>
using namespace std;
class Inventory {
public:
enum Fruit { APPLE, BANANA, GRAPE, ORANGE };
};
int main() {
if(Inventory::APPLE < Inventory::Fruit::ORANGE) {
cout << "Comparing C++03 'enum' with C++11 'enum class' is like comparing apples and oranges." << endl;
}
}

output:
Comparing C++03 'enum' with C++11 'enum class' is like comparing apples and oranges.


Footnotes
-->
[*1] Trivia: An alternative way to declare an enum class is ... you guessed it, enum struct. The C++ standards committee seems to be obsessed with emphasizing the class-struct (near-)equivalence. Rather surprisingly, they did not choose to allow template<struct S>, perhaps to help interviewers. :wink:
[*2] There are at least two uses of enum when "an enum is an int" is not absurd, but useful. The enum can be used to declare named constants of an integral type. It can also be used to group together a set of closely related status flags, which can then be encoded as successive powers of two to ease coding by using bitwise operators.
[*3] gcc has been making steady progress in implementing support for the new C++11 features, with each new version ticking off some boxes. Read more about the evolution of gcc's C++11 support on their website here http://gcc.gnu.org/projects/cxx0x.html. At the time of writing, ideone.com used gcc-4.5.1 as the online C++11 compiler, which does not support enum class forward declaration. Hence, the example code does not provide an ideone.com link. I compiled this code using gcc-4.6.3 on my laptop.

No comments:

Post a Comment