Is there a compile-time way to detect / prevent duplicate values within a C/C++ enumeration?
The catch is that there are multiple items which are initialized to explicit values.
Background:
I've inherited some C code such as the following:
#define BASE1_VAL (5) #define BASE2_VAL (7) typedef enum { MsgFoo1A = BASE1_VAL, // 5 MsgFoo1B, // 6 MsgFoo1C, // 7 MsgFoo1D, // 8 MsgFoo1E, // 9 MsgFoo2A = BASE2_VAL, // Uh oh! 7 again... MsgFoo2B // Uh oh! 8 again... } FOO;
The problem is that as the code grows & as developers add more messages to the MsgFoo1x
group, eventually it overruns BASE2_VAL
.
This code will eventually be migrated to C++, so if there is a C++-only solution (template magic?), that's OK -- but a solution that works with C and C++ is better.
Two enum names can have same value. For example, in the following C program both 'Failed' and 'Freezed' have same value 0.
Enums can not have duplicate values.
By definition, the enumeration member values are unique. However, you can create different member names with the same values.
A scoped enum looks exactly as a traditional enum except that the keyword class (or struct – the two keywords are interchangeable in this context) appears between the keyword enum and the enum name, as shown in the following example: enum class Color //C++11 scoped enum.
There are a couple ways to check this compile time, but they might not always work for you. Start by inserting a "marker" enum value right before MsgFoo2A.
typedef enum { MsgFoo1A = BASE1_VAL, MsgFoo1B, MsgFoo1C, MsgFoo1D, MsgFoo1E, MARKER_1_DONT_USE, /* Don't use this value, but leave it here. */ MsgFoo2A = BASE2_VAL, MsgFoo2B } FOO;
Now we need a way to ensure that MARKER_1_DONT_USE < BASE2_VAL
at compile-time. There are two common techiques.
It is an error to declare an array with negative size. This looks a little ugly, but it works.
extern int IGNORE_ENUM_CHECK[MARKER_1_DONT_USE > BASE2_VAL ? -1 : 1];
Almost every compiler ever written will generate an error if MARKER_1_DONT_USE is greater than BASE_2_VAL. GCC spits out:
test.c:16: error: size of array ‘IGNORE_ENUM_CHECK’ is negative
If your compiler supports C11, you can use _Static_assert
. Support for C11 is not ubiquitous, but your compiler may support _Static_assert
anyway, especially since the corresponding feature in C++ is widely supported.
_Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap.");
GCC spits out the following message:
test.c:16:1: error: static assertion failed: "Enum values overlap." _Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap."); ^
I didn't see "pretty" in your requirements, so I submit this solution implemented using the Boost Preprocessor library.
As an up-front disclaimer, I haven't used Boost.Preprocessor a whole lot and I've only tested this with the test cases presented here, so there could be bugs, and there may be an easier, cleaner way to do this. I certainly welcome comments, corrections, suggestions, insults, etc.
Here we go:
#include <boost/preprocessor.hpp> #define EXPAND_ENUM_VALUE(r, data, i, elem) \ BOOST_PP_SEQ_ELEM(0, elem) \ BOOST_PP_IIF( \ BOOST_PP_EQUAL(BOOST_PP_SEQ_SIZE(elem), 2), \ = BOOST_PP_SEQ_ELEM(1, elem), \ BOOST_PP_EMPTY()) \ BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(data, BOOST_PP_ADD(i, 1))) #define ADD_CASE_FOR_ENUM_VALUE(r, data, elem) \ case BOOST_PP_SEQ_ELEM(0, elem) : break; #define DEFINE_UNIQUE_ENUM(name, values) \ enum name \ { \ BOOST_PP_SEQ_FOR_EACH_I(EXPAND_ENUM_VALUE, \ BOOST_PP_SEQ_SIZE(values), values) \ }; \ \ namespace detail \ { \ void UniqueEnumSanityCheck##name() \ { \ switch (name()) \ { \ BOOST_PP_SEQ_FOR_EACH(ADD_CASE_FOR_ENUM_VALUE, name, values) \ } \ } \ }
We can then use it like so:
DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday) (1)) ((Tuesday) (2)) ((Wednesday) ) ((Thursday) (4)))
The enumerator value is optional; this code generates an enumeration equivalent to:
enum DayOfWeek { Monday = 1, Tuesday = 2, Wednesday, Thursday = 4 };
It also generates a sanity-check function that contains a switch statement as described in Ben Voigt's answer. If we change the enumeration declaration such that we have non-unique enumerator values, e.g.,
DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday) (1)) ((Tuesday) (2)) ((Wednesday) ) ((Thursday) (1)))
it will not compile (Visual C++ reports the expected error C2196: case value '1' already used).
Thanks also to Matthieu M., whose answer to another question got me interested in the Boost Preprocessor library.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With