Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

When enum Just Isn't Enough: Enumeration Classes for C++


May 2003/When enum Just Isn't Enough: Enumeration Classes for C++

Accessing Resources through Symbolic Constants

If you're writing even a moderately ambitious application, you will find that it needs to access many resources of various types, for example sounds and text messages. In a large application, individual resources can easily number in the hundreds. Typically, they will be referenced by a numeric value. For the time being, I'll suppose you're using ints to refer to resources, but I'll generalize later.

Assume that you're working on a game, which uses pre-rendered graphics, player messages, and sound effects. High-level functions allow you to write PlaySound(1001); and DisplaySprite(435);, but of course you wouldn't use magic numbers. You might declare symbolic constants instead, as in const int explosionSnd = 1001;. This is definitely better, but not type-safe: nothing prevents you from writing DisplaySprite(explosionSnd); when you meant to write DisplaySprite(explosionSprite);.

The traditional solution to this lack of type-safety is to group your constants inside enums. Given an enum for sounds, e_Sounds, and one for sprites, e_Sprites, you can declare a function to take an argument of the proper type, as in void DisplaySprite(e_Sprite);. Once this has been done, the compiler will ensure that you cannot accidentally pass a sound constant to a sprite display function.

Enumerations Don't Always Fit the Bill

You might feel this is the best you can do, and all that needs to be done. But the enum-based approach is still insufficient for serious applications. The problem is that enum is an unintelligent structure: C++ does not allow you to programmatically extract anything significant from it. This makes it impossible to automate operations on enum, when this is precisely what needs to be done for many applications. Consider that huge game with its hundreds of sounds: You'd certainly like to be able to verify that every sound constant referenced in the code corresponds to a sound that is actually available in the current version of the application. The inverse task is also important: If those 25 new sounds that were just added in the new version are never even played, you'd want your automated tests to notice that.

Now it is possible to iterate over enum, so you might think that the first test, at least, is trivial: just write a for loop. Listing 1 illustrates this ideal situation.

Listing 1

enum Sounds {
  explosionSnd = 101,
  attackSnd = 102,
  ... // each constant increases by 1
  victorySnd = 185
}

Sounds firstSnd = explosionSnd;
Sounds lastSnd = victorySnd;

for ( int theSound = firstSnd;
theSound <= lastSnd; ++theSound )
  CheckSoundIsPresent(theSound); 

Real-world programs do not look anything like this, however. Both initial design considerations and maintenance issues conspire to produce a situation like that in Listing 2. When you've got even one "hole" in your enum, you're in trouble.

Listing 2

enum Sounds {
  // Human sounds in [100, 200[
  explosionSnd = 101,
  attackSnd = 102,
  // ricochetSnd = 103, // removed, beta testers hated it
  deathSnd = 104,
  // Alien sounds in [200, 300[
  alienAttackSnd = 201,
  ...
  alienDeathSnd = 227,
  // Miscellaneous sounds >= 500
  victorySnd = 500
};

Using straightforward iteration with ints over an enumeration that is not continuous is legal C++; it just gives erroneous results: intermediate, "nameless" values are legal even though they are meaningless. Now, since enum is a user-defined type, it is possible to define an increment/decrement operator for it [1]. This makes it possible to iterate through enum using a for loop with a specific operator++. However, this is a false solution: It can indeed be very useful for small, continuous enums — for instance, if you have an enum for the days of the week, it's very convenient to define an operator++ that always returns the next day of the week — but with a large, discontinuous enumeration things go rapidly sour.

Suppose you've managed to write an operator++ for a discontinuous enum — a tedious and error-prone task in the first place. Even then, maintenance issues are still a nightmare: If someone adds a sound constant to the e_Sounds enum in an "empty slot," your operator++ will skip right over it without warning. If this constant is somehow passed to your operator++, it will fail to recognize it and at best give a wrong result, if it doesn't cause a runtime error instead.

Even if you ignore these difficulties, the second test you wanted to run remains infeasible: You still cannot tell whether a given value is a valid part of an enumeration or not. You could of course write a custom function to do this, and then you'd suffer from the very same maintenance nightmares I was just talking about. Unless you enjoy wasting hours debugging, something better is clearly indicated.

An Industrial-Strength Solution

What you need is an intelligent enumeration: one that knows its minimum and maximum and its valid ranges. Such an enumeration will allow you to iterate over it automatically, because it will furnish its own standard mechanism for doing so, which will obviate the need to write and maintain one yourself. In other words, you need a class that allows you to unify symbolic constants into a single type.

The basic idea is to make each constant of your enum be a class-static constant instance of a class. This class will keep a sorted list of its own instances in a class-static container; STL's std::set is ideal since it keeps its contents sorted. Iterating over the enumeration becomes a matter of iterating over the set. Since the set is sorted, it becomes trivial to obtain the enumeration's maximum and minimum. This design is proof against maintenance problems, since the set is built from the constants defined in the code: whether constants are added or removed, the set will always be correctly built.

There is a potential thorn in your side: it's crucially important that it be impossible to add to the enumeration during the course of the program. However, you may want to have local variables of the type of the enumeration, just like with regular enums. The first requirement seems to imply you can only construct class instances before main is executed, and the other requires construction during the course of the program. In fact this simply forces you to distinguish between class-static instances (constructed before main is executed from their int value, immutable) and local instances (constructed dynamically by copying from a class-static instance, mutable).

I've made use of templates for implementing this design, since you'll typically use more than one such enumeration class. This template is meant to be used with the well-known idiom (which as far as I know lacks a standard name) whereby class A is derived from a class B templated on A itself.

Listing 3 presents a class template that implements the desired features. The class has a private: constructor from int. Only the class-static instances may be defined this way. The copy constructor (synthesized by the compiler) makes it possible to instantiate local variables that are copies of class-static instances. The class keeps a set of its static instances, which is always sorted. STL thus does all the hard work for you, and your implementation can remain simple and intuitive, or nearly so.

Listing 3

#include <functional>
#include <set>
template <class T>
class Enum {
private:
  // Constructors
  explicit Enum(int Value);
  // Predicate for finding the corresponding instance
  struct Enum_Predicate_Corresponds:
   public std::unary_function<const Enum<T>*, bool> {
      Enum_Predicate_Corresponds(int Value): m_value(Value) { }
      bool operator()(const Enum<T>* E)
      { return E->Get_Value() == m_value; }
   private:
      const int m_value;
  };
  // Comparison functor for the set of instances
  struct Enum_Ptr_Less:
   public std::binary_function<const Enum<T>*, const Enum<T>*, bool> {
      bool operator()(const Enum<T>* E_1, const Enum<T>* E_2)
      { return E_1->Get_Value() < E_2->Get_Value(); }
  };
public:
  // Compiler-generated copy constructor and operator= are OK.
  typedef std::set<const Enum<T>*, Enum_Ptr_Less> instances_list;
  typedef instances_list::const_iterator const_iterator;
  // Access to int value
  int Get_Value(void) const { return m_value; }
  static int Min(void) { return (*s_instances.begin())->m_value; }
  static int Max(void) { return (*s_instances.rbegin())->m_value; }
  static const Enum<T>* Corresponding_Enum(int Value)
  { const_iterator it = find_if(s_instances.begin(), s_instances.end(), 
   Enum_Predicate_Corresponds(Value));
   return (it != s_instances.end()) ? *it : NULL; }
  static bool Is_Valid_Value(int Value) { return Corresponding_Enum(Value) != NULL; }
  // Number of elements
  static instances_list::size_type size(void) { return s_instances.size();
}
  // Iteration
  static const_iterator begin(void) { return s_instances.begin(); }
  static const_iterator end(void) { return s_instances.end(); }
private:
  int m_value;
  static instances_list s_instances;
};
template <class T>
inline Enum<T>::Enum(int Value):
  m_value(Value)
{
  s_instances.insert(this);
}

What may not be so intuitive are the two structs in the private section, Enum_Predicate_Corresponds and Enum_Ptr_Less. The first is a predicate for use with find_if in the Corresponding_Enum method. It's not strictly necessary, since I could have used an explicit loop comparing the members of the set with the sought value, but find_if is better style. The second struct, Enum_Ptr_Less, is absolutely necessary to keep your set sorted in the order you want. As Scott Meyer points out in [2], you have to be careful when dealing with associative containers of pointers. The containers will by default be sorted by comparing the pointer values, which is almost never what you want. So you have to give the set a comparison object whose operator(), when given two members of the set, will return whether the first one is less than the other according to the appropriate criterion. In our example, you need to compare the results of the Get_Value function.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.