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 int
s 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 enum
s. 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 int
s 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 enum
s 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 enum
s. 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 struct
s 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.