The enum
feature of C/C++ is far from new. It is very useful for defining a specific set of values that a simple integer may take, which can lead to clearer, more concise code when used appropriately. Many compilers are capable of warning about common errors associated with enum
use, such as not including case
statements for all possible enum
values in a switch
statement that has no default
clause. In many respects, an enum
acts like a set, but being essentially just a glorified int
, it lacks any of the container features of something like std::set
.
The developer typically faces a tradeoff between performance and functionality when deciding between an enum
or some kind of set-like container. There are some (often common) situations, however, where an enum
can still be treated like a container, thanks to features made available in C++11.
An enum
without assigned values
For developers coming from more of a C background or C++ developers who are not so familiar with the STL, it is not unusual to encounter code such as the following:
enum Shape
{
Circle,
Square,
Triangle,
Oval,
Polygon,
NumShapes
};
// Variant 1
for(Shape s=(Shape)0; s!=NumShapes; s=(Shape)(s+1))
{
// ...
}
// Variant 2
for(int i=0; i!=NumShapes; ++i)
{
Shape s = (Shape)i;
// ...
}
This sort of use of enum
is not ideal. It includes a count in the list of Shape
values, which reduces the usefulness of the enum
in defining all meaningful shapes because a Shape
object can hold a value which is not an actual valid shape at all. This diminishes one of the main advantages of using an enum
, which is to define precisely the set of allowable values and write code which only needs to consider those values as possibilities. Another disadvantage of this sort of approach is that it requires casting between a raw int
and Shape
, which reduces the robustness of the code. A slightly better alternative that is sometimes used is to store the count outside of the enum
:
enum Shape
{
Circle,
Square,
Triangle,
Oval,
Polygon
};
const int NumShapes = 5;
In this case, Shape
specifies only allowable values, which is good, but the separate counter is a fragile way to track the number of defined values. If the enum
grows large, it would be easy for a developer to forget to update the NumShapes
value and tracking down this bug could be quite difficult. This approach also does nothing to help us avoid the undesirable cast between int
and Shape
when iterating over the set of enumerated values.
It would be better to dispense with the counter altogether and treat the enum
more like a container, using iterators or ranges (the latter being a C++11 feature). Conveniently, std::initializer_list
(see previous article) allows us to do exactly that, with a little bit of help from a C pre-processor feature called variadic macros. Variadic macros are the macro equivalent of variadic arguments which most programmers know from using printf()
and friends. Simply place ...
at the end of your macro’s argument list and then use __VA_ARGS__
wherever you want the list of remaining arguments to appear.
#define SequentialEnum(Name,...) \
enum Name { __VA_ARGS__ }; \
namespace \
{ \
const std::initializer_list<Name> Name##List { __VA_ARGS__ }; \
};
SequentialEnum(Shape,
Circle,
Square,
Triangle,
Oval,
Polygon
);
for (Shape s : ShapeList)
{
// loop body...
}
The SequentialEnum
macro conveniently allows the developer to define the set of enum
values once and to be able to iterate over them like a container. This is both concise and robust, without sacrificing readability.
An enum
with assigned values
All of the above examples share the common assumption that the enum
values have the default numbering. This applies to many of the common uses of an enum
, but for an enum
which has any explicitly numbered values, none of the above strategies will work. Unfortunately, there is no easy way to automate creating some kind of initializer list when at least one of the enumerants is assigned a value. The method we used previously relied on the format of an enum
list being the same as that for an initializer list. When enumerants are assigned a value, the only clean alternative is to manually specify an appropriate initializer list explicitly.
enum Shape
{
Circle = 23,
Square = 17,
Triangle = 92,
Oval = 4,
Polygon = 15
};
static const auto ShapeList
{
Circle,
Square,
Triangle,
Oval,
Polygon
};
static const auto CurvedShapes{ Circle, Oval };
static const auto StraightShapes{ Square, Triangle, Polygon };
We’ve used auto
as a convenience instead of explicitly specifying the std::initializer_list
type. Whether you use a static
variable or an anonymous namespace is also largely a matter of personal choice.
The above has the obvious drawback that the list of enumerants is specified twice, which is arguably no better than when we manually specified the number of enum
values near the beginning of this article. One thing it does still do, however, is allow the enum
to be treated like a container and iterate over the defined values in a very clean and concise way. For enum
s with only a small number of values, this may be an acceptable tradeoff. As shown above, we can also use this same technique to define subsets of the enum
values which can then be referred to consistently across the code base.
Closing comments
C++11’s ranges and std::initializer_list
in particular are very useful tools which enable convenient iteration over enum
values. Their ability to build containers of values at compile time provide many of the advantages of a set-like container without having to pay the performance tradeoffs.
Neat! Thanks for the trick 🙂
Macro is missing trailing backslashes.
Thanks, I think they were lost in an earlier WordPress update/migration. Fixed now.