Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect when multiple enum items map to same value

Tags:

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.

like image 461
Dan Avatar asked Apr 05 '10 04:04

Dan


People also ask

Can multiple enum have same value?

Two enum names can have same value. For example, in the following C program both 'Failed' and 'Freezed' have same value 0.

Can enum have duplicate values in C#?

Enums can not have duplicate values.

Can enums have the same value python?

By definition, the enumeration member values are unique. However, you can create different member names with the same values.

What is a scoped enumeration?

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.


2 Answers

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.

Negative size arrays

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 

Static assertions

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.");  ^ 
like image 70
Dietrich Epp Avatar answered Oct 03 '22 06:10

Dietrich Epp


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.

like image 33
James McNellis Avatar answered Oct 03 '22 06:10

James McNellis