Advertisement

More on enumerations

June 17, 2003

Dan_Saks-June 17, 2003

More on enumerations
Here are some additional tips for using enumerations as loop counters and for displaying enumeration names.

My recent columns have been about using enumeration variables as loop counters.[1,2,3] In particular, I've been exploring styles for declaring enumerations so that you can use them in loops that are readable, maintainable, efficient, and portable.

While not every enumeration is appropriate as the type of a loop counter, those that have contiguous values are. Thus, it's reasonable to use:

enum month
{
January, ..., December
};
typedef enum month month;

for writing loops such as:

month m;
for (m = January; m <= December; ++m)
...

This loop compiles as C. It also compiles as C++, but only if you define a prefix operator++ for an argument of type month.

When you deal with types representing everyday phenomena, such as months, it's easy to remember which is the first (lowest) value and which is the last (highest).

On the other hand, many enumerations don't have obvious minimum and maximum values. For example, how do you remember which are the first and last values of:

enum currency
{
CAD, CHR, EUR, GBP, JPY, SFR, USD
};

and how can you write loops that continue to work even if you add, remove, or reorder the enumeration constants?

Ada has a solution to this problem in the form of predefined attributes 'first and 'last, which you can use to write:

for c in currency'first..currency'last
loop

C and C++ have no such predefined attributes, but you can imitate such attributes by declaring additional enumeration constants representing the minimum and maximum values for the type, as in:

enum currency
{
currency_before = -1,
CAD, CHR, EUR, GBP, JPY, SFR, USD,
currency_after,
currency_min = currency_before + 1,
currency_max = currency_after - 1,
};

Then you can write loops such as:

c = currency_min;
for (; c <= currency_max; ++c)
...

which counts up through the currencies without regard to which currency is first.

As I explained in April, defining an extra symbol such as currency_after ensures that the loop can safely increment c one beyond currency_max.[2] Similarly, defining currency_before ensures that a loop such as:

c = currency_max;
for (; c >= currency_min; —c)
...

can safely decrement c one beyond currency_min.

A universal style for loops
If asked to write a loop that does something for each value of i from 0 to n-1, inclusive, most of us would write something like:

for (i = 0; i < n; ++i)
// do something

If asked to write a loop that iterates from n-1 down to 0, most of us would write something like:

for (i = n-1; i >= 0; --i)
// do something

Koenig and Moo observed that loops in these styles don't always work when i is an unsigned integer or a Standard C++ Library iterator.[4] For example, when i is an unsigned integer, the condition i >= 0 is always true and the preceding loop runs forever.

Koenig and Moo recommend a universal style for loops that count up and a slightly different universal style for loops that count down. When the loop counter i is an integer counting up from 0 to n-1, a loop in the universal style looks like:

for (i = 0; i != n; ++i)
// do something

When i is an iterator for a container x, a loop in the universal style that traverses x from front to back looks like:

for (i = x.begin(); i != x.end(); ++i)
// do something

These loops use != rather than <= because <= is not defined for certain iterator types.

Universal loops that count down look very unconventional. When i is an integer, a loop that counts from n-1 down to 0 looks like:

for (i = n; i != 0;)
{
--i;
// do something
}

or:

i = n;
while (i != 0)
{
--i;
// do something
}

When i is an iterator for a container x, a loop that traverses x in reverse order looks like:

i = x.end();
for (i != x.begin())
{
--i;
// do something
}

Despite their unorthodox appearances, these loops have an appealing property: they never assign i a value below i's value on the final iteration. This eliminates any concerns about overflow or wraparound.

You can use Koenig and Moo's universal loops with enumerations. Since this looping style is designed to accommodate the iterator conventions of the Standard C++ Library, symbols for enumeration limits used with these loops should try to follow iterator naming conventions. For instance, for a type such as currency, currency_begin represents the lowest currency value and currency_end represents "one beyond" the highest currency value, as in:

enum currency
{
currency_begin,
CAD = currency_begin,
CHR, EUR, GBP, JPY, SFR, USD,
currency_after
};

Using these conventions, a universal loop that counts up through the values looks like:

c = currency_begin;
for (; c != currency_end; ++c)
// do something

A universal loop that counts down looks like:

c = currency_end;
while (c != currency_begin)
{
--c;
// do something
}

Since the latter loop doesn't decrement c when c is equal to currency_begin, you don't need an extra value such as currency_before to ensure the decrement has well-defined behavior.

I'm not wedded to any particular naming scheme for specifying the limits of an enumeration type. The scheme you use depends very much on the way you use the type, especially the way you write loops. As always, it's best to pick a preferred style and use it as consistently as you can.

Displaying enumeration values
Neither C nor C++ provides symbolic output for enumerations. That is, given:

enum day
{
Sunday, ..., Saturday
};
typedef enum day day;
day d = Monday;

it'd be nice if we could display the value of d as Monday by writing something like:

printf("%e", d);

But there's no format specifier for enumerations. (And %e is already taken for floating pointer numbers.)

C and C++ promote enumeration values to integers. Thus, you can display d's integer value using:

printf("%d", d); /* in C */

or:

cout << d;// in C++

but then the output won't be mnemonic.

Once again, Ada provides a nice model for the behavior we'd like. In Ada, e'image returns the string representation of enumeration value e. The library function put(e) displays the image of e.

You can simulate the image attribute using a constant array. For example, in C or C++ you can declare:

char const *const day_image[] =
{
"Sunday", "Monday", ...
};

Then you can display day d as a symbol rather than an integer in C using:

printf("%s", day_image[d]);

or in C++ using:

cout << day_image[d];

Of course, an image array works only when the enumeration values are contiguous and start at zero, as in the case of day as declared above. Suppose the lowest enumeration constant has a nonzero value, as in:

enum month
{
January = 1, ..., December
};
typedef enum month month;

In this case, you must use the expression month_image[m - January] to convert month m to a printable string.

A function provides a safer and more general conversion mechanism. A function can hide the details of the conversion, as well as intercept invalid enumeration values. Listing 1 shows what that function might look like for month.

Listing 1: A function that converts a month to a text image

char const *month_image(month m)
{
static char const *const _image[] =
{
"January", "February", ...
};

if (m <= January || m >= December)
return "invalid month";
else
return _image[m - January];
}

In C++, you can take advantage of function name overloading and name every image function as just image. This provides a uniform attribute notation much like Ada's. You can simply write image(m) instead of month_image(m) and image(d) instead of day_image(d).

Besser presents much more elaborate and general C++ machinery for converting enumeration values to printable strings.[5] Using a combination of classes and macros, you declare an enumeration type day as:

ENUM7(day, Sunday, ..., Saturday);

The macro call automatically generates an image array along with the enumeration type definition. Then you can write loops such as:

d = day::begin();
for (; d != day::end; ++d)
std::cout << d.text() << std::endl;

In this scheme, d.text() returns the image of d.

I think this approach merits a careful look, but my initial impression is that it's more elaborate than what many embedded developers would want.

Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. You can write to him at dsaks@wittenberg.edu.

References
1. Saks, Dan. "Enumerations as Counters," Embedded Systems Programming, December 2002, p. 36.
2. Saks, Dan. "Well-Behaved Enumerations," Embedded Systems Programming, April 2003, p.41.
3. Saks, Dan. "Enumerations Q & A," Embedded Systems Programming, May 2003, p. 27.
4. Koenig, Andrew and Barbara Moo. "Simple Loops, Generalized," The C/C++ Users Journal, June 2003, p. 50.
5. Besser, Mitch. "Generic Printable ENUM++," C/C++ Users Journal: www.cuj.com/boost/2003/0306/besser/besser.htm?topic=boost

Loading comments...