In The Design and
Evolution of C++ Bjarne Stroustrup gives interesting details about the
history of enumerations in C and C++. Apparently, they were not part of
the original C design and were reluctantly added later. Bjarne also admits
that he was not very interested in enumerations as a feature and added
them with some minor modifications needed to fit into the concept of
user-defined types. It should not be a surprise, therefore, that enumeration
types abound in all sorts of little defects and inconveniences. Consider,
for example, the following code snippet from the C++ standard library
(namespace std
is omitted):
class ios_base { // ... enum event { erase_event, imbue_event, copyfmt_event }; // ... }
Nobody will argue that the following code is what was meant
enum event { erase, imbue, copyfmt };
And ios_base::event::erase
is more consistent than
ios_base::event_erase
.
In this article I try to identify problems with C++ enumerations and present possible solutions using the existing language constructs.
Our guinea pig for the problem illustrations will be the following enumeration type:
enum Color { red, green, blue };
Each identified issue is assigned a number so that it can be referred to during the solutions analysis.
Enumerators (e.g. red
, green
, blue
)
are in the same scope as the enumeration type itself. In other words,
enumerators are not encapsulated. The following example shows one implication
of this fact:
enum Direction { left, right }; enum Alignment { left, // error right, // error center };
A common way to work around this problem is to prefix each enumerator with the type name:
enum Direction { direction_left, direction_right }; enum Alignment { alignment_left, alignment_right, alignment_center };
It is interesting to note that enumerations break the rule of braces
. In C++ (and in C, for that matter) an opening brace ({
)
signifies the beginning of a new naming scope. In every construct but
enumeration.
Instances of enumeration type cannot be default-initialized or force
their user to initialize them. The instance of enumeration type is
initialized to 0
if explicitly requested. This doesn't help
much, though, as illustrated by the following example:
enum Color { black, // black happened to be 0 red, green, blue }; template <typename T> struct S { T t_; S () : t_ () {} }; void f () { Color c = Color (); // c is 0 (black) S<Color> s; // s::t_ is 0 (black) }
Note, however, that the following code doesn't work as one may have expected:
void f () { Color c1 (); // function declaration c1 = blue; // error Color c2; // c2 has undetermined value }
There is no uniform way to associate any additional information with the
enumeration type. Data like the number of elements or labels for enumerators
(e.g. "red"
, "green"
, "blue"
) can be
useful. It would be nice to be able to write something along these lines:
void f (std::string const&); void g () { Color c (Color::red); f (c.label ()); if (Color::size == 3) { // ... } for (Color::iterator i (Color::begin ()), e (Color::end ()); i != e; ++i) { // ... } }
Enumeration types are subject to the implicit integral promotion. This allows us to write meaningless code which gets away undiagnosed:
void f1 (int) { Color c (blue); if (c || c > red) // ok { // ... } } void f2 () { f1 (red + green); // ok }
The first version of this article presented some informal engineering of solutions for the problems above. However, such an approach is inconclusive and unconvincing, especially when there is no single answer that meets all the criteria. When we are guided by our intuition, reasons for the decisions we make and the optimality of those decisions are always in question. To attain some degree of formality, I decided to synthesize an optimal solution using multi-criteria optimization. In this section I only present results. If you are interested in the details of the process I invite you to read Class-type Enumeration Synthesis.
The synthesis process resulted in two alternatives. In this section we will perform further analysis and discuss which one to choose depending on our needs. The first alternative combines solutions for all the problems above and some degree of backward compatibility:
class Color { public: static Color const red, green, blue; enum Value { red_l, green_l, blue_l }; Value integral () const; char const* label () const; friend bool operator== (Color const& a, Color const& b); friend bool operator!= (Color const& a, Color const& b); private: Color (Value v); Value v_; }; std::ostream& operator<< (std::ostream& o, Color c);
Here is how we would use this type in our problematic cases:
void f (Color) { cerr << "color" << endl; } void f (int) { cerr << "int" << endl; } void f () { // 1 { // Color c (red); // error: red undeclared Color c (Color::red); } // 2 { // Color c; // error: no default c-tor Color c (Color::red); c = Color::blue; } // 3 { Color c (Color::red); cerr << c << " " << Color::green << endl; } // 4 { Color c (Color::red); f (c); // calls f (Color) f (Color::red); // calls f (Color) // c + Color::red; // error: no operator+ // if (c) {} // error: no conversion // from Color to bool if (c == Color::red && c != Color::blue) { cerr << "ok" << endl; } } // 5 { Color c (Color::red); switch (c.integral ()) { case Color::red_l: cerr << "r" << endl; break; case Color::green_l: cerr << "g" << endl; break; case Color::blue_l: cerr << "b" << endl; break; } } }
The complete source code for the first alternative is available. As you can see the usage of this class-type enumeration in combination with switch statement is cumbersome. This should not be a surprise to you since C++ switch mechanism is restricted to an integral-convertible type as a switch argument and an integral constants as a switch label. The optimal synthesis kind of suggests that the best way to handle switch is by providing special mechanisms.
In cases where we don't need to support switch statement, we can lighten up our class-type enumeration and end up with the following elegant code:
class Color { public: static Color const red, green, blue; char const* label () const; friend bool operator== (Color const& a, Color const& b); friend bool operator!= (Color const& a, Color const& b); private: enum Value { red_, green_, blue_ } v_; Color (Value v); };
The complete source code for the first alternative without switch support is also available.
The second alternative provides a higher degree of backward compatibility but fails to address the fourth problem. From the optimal synthesis' point of view this alternative is neither better nor worse than the first. However, the fourth problem is important and leaving it unsolved makes our enumeration a dangerous type. I don't recommend that you use this alternative and present it here only for reference.
class Color { public: enum Value { red, green, blue }; Color (Value v); operator Value () const; operator char const* () const; private: Value v_; };
The following code shows how we would use this type in our problematic cases:
void f (Color) { cerr << "color" << endl; } void f (int) { cerr << "int" << endl; } void f () { // 1 { // Color c (red); // error: red undeclared Color c (Color::red); } // 2 { // Color c; // error: no default c-tor Color c (Color::red); c = Color::blue; } // 3 { Color c (Color::red); cerr << c << " " << Color::green << endl; // prints "red 1"! } // 4 { Color c (Color::red); f (c); f (Color::red); // calls f (int) // c + Color::red; // ambiguous operator+ if (c) {} // ok if (c == Color::red && c != Color::blue) { cerr << "ok" << endl; } } // 5 { Color c (Color::red); switch (c) { case Color::red: cerr << "r" << endl; break; case Color::green: cerr << "g" << endl; break; case Color::blue: cerr << "b" << endl; break; } }
The complete source code for the second alternative is available.
Copyright © 2003, 2004 Boris Kolpackov. See license for conditions.
Last updated on Jan 26 2004