13

Let's say I have something like this :

enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES};

CardColor MyColor = static_cast<CardColor>(100);

Is there a (simple) way to detect, either at compile-time or at runtime, that the value of MyColor doesn't correspond to any enumerated value ?

And more generally, if the enum values are not following each other, for instance :

enum CardColor { HEARTS = 0, DIAMONDS, CLUBS = 4, SPADES};
Jérôme
  • 26,567
  • 29
  • 98
  • 120
  • @CashCow : well, both ! I updated my question. – Jérôme Jan 25 '11 at 16:26
  • @James : I know I shouldn't do that, but that doesn't stop me being curious ! :-) – Jérôme Jan 25 '11 at 16:31
  • There's a very good reason for the lack of implicit conversion from an enum's underlying type to the enum's type. So when working with CardColor think in CardColor terms not an integer that can be explicitly converted to CardColor. – Eugen Constantin Dinca Jan 25 '11 at 16:41

6 Answers6

16

CashCow presents a decent answer to this question: it's certainly straightforward to write a custom function to perform a checked cast.

Unfortunately, it's also a lot of work and you must make sure to keep it synchronized with the enumeration so that the list of enumerators in the enumeration definition is the same as the list of enumerators in the checked cast function. You also have to write one of these for each enumeration to which you want to be able to perform a checked cast.

Instead of doing all this manual work, we can automate generation of all of this code using the preprocessor (with a little help from the Boost Preprocessor library). Here is a macro that generates an enumeration definition along with a checked_enum_cast function. It's probably a bit scary looking (code generation macros are often horrible to look upon), but it's an extremely useful technique to become familiar with.

#include <stdexcept>
#include <boost/preprocessor.hpp>

// Internal helper to provide partial specialization for checked_enum_cast
template <typename Target, typename Source>
struct checked_enum_cast_impl;

// Exception thrown by checked_enum_cast on cast failure
struct invalid_enum_cast : std::out_of_range 
{ 
    invalid_enum_cast(const char* s)
        : std::out_of_range(s) { }
};

// Checked cast function
template <typename Target, typename Source>
Target checked_enum_cast(Source s)
{
    return checked_enum_cast_impl<Target, Source>::do_cast(s);
}

// Internal helper to help declare case labels in the checked cast function
#define X_DEFINE_SAFE_CAST_CASE(r, data, elem) case elem:

// Macro to define an enum with a checked cast function.  name is the name of 
// the enumeration to be defined and enumerators is the preprocessing sequence
// of enumerators to be defined.  See the usage example below.
#define DEFINE_SAFE_CAST_ENUM(name, enumerators)                           \
    enum name                                                              \
    {                                                                      \
        BOOST_PP_SEQ_ENUM(enumerators)                                     \
    };                                                                     \
                                                                           \
    template <typename Source>                                             \
    struct checked_enum_cast_impl<name, Source>                            \
    {                                                                      \
        static name do_cast(Source s)                                      \
        {                                                                  \
            switch (s)                                                     \
            {                                                              \
            BOOST_PP_SEQ_FOR_EACH(X_DEFINE_SAFE_CAST_CASE, 0, enumerators) \
                return static_cast<name>(s);                               \
            default:                                                       \
                throw invalid_enum_cast(BOOST_PP_STRINGIZE(name));         \
            }                                                              \
            return name();                                                 \
        }                                                                  \
    };

Here is how you would use that with your CardColor example:

DEFINE_SAFE_CAST_ENUM(CardColor, (HEARTS) (CLUBS) (SPADES) (DIAMONDS))

int main()
{
    checked_enum_cast<CardColor>(1);   // ok
    checked_enum_cast<CardColor>(400); // o noez!  an exception!
}

The first line replaces your enum CardColor ... definition; it defines the enumeration and provides a specialization that allows you to use checked_enum_cast to cast integers to CardColor.

This may look like a lot of hassle just to get a checked cast function for your enums, but this technique is very useful and extensible. You can add functions that do all sorts of things. For example, I have one that generates functions to convert enumerated types to and from string representations and functions that perform several other conversions and checks that I use for most of my enumerations.

Remember, you have to write and debug that big, ugly macro just once, then you can use it everywhere.

Community
  • 1
  • 1
James McNellis
  • 348,265
  • 75
  • 913
  • 977
9

Simplest run-time solution would be to not use static_cast but use a function that does the check for you. If you put your enum inside a class you can do that through the class. Something like:

class CardCheck
{
public:
  enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES };

  explicit CardCheck( int x ) : c( static_cast< CardColor >( x ) )
  {
     switch( c )
     {
       case HEARTS: case DIAMONDS: case CLUBS: case SPADES:
          break;

       default:
         // assert or throw
    }
  }

  CardColor get() const
  {
     return c;
  }

 private:
  CardColor c;      
};
CashCow
  • 30,981
  • 5
  • 61
  • 92
  • 1
    The cast should probably take place after the check: it could potentially result in an overflow (I think). – James McNellis Jan 25 '11 at 17:02
  • 2
    Interestingly we did something like this in our code to validate integers being cast to enums. One compiler decided to optimise the switch statement, presumably because all the possible cases were covered so it assumed it was always valid. Perhaps a good reason to include CARD_COUNT. Then there is at least one possible bad value and the compiler won't optimise. – CashCow Jan 27 '11 at 15:17
5

clang have support for dynamic overflow checks. See -fsanitize=enum switch. A program compiled with this switch will signal enum assignment errors through stderr output. This will allow you to do debug tests. It is not suitable to test suspicious input in official build.

ZAB
  • 963
  • 10
  • 18
3

It is common to have an additional element at the end of the enum indicating the number of items in it. You can use that value to check at runtime if the value is valid:

enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES, CARDS_COUNT};

CardColor MyColor = static_cast<CardColor>(100);

if (MyColor >= CARDS_COUNT) {
    /* Invalid value */
}
vz0
  • 32,345
  • 7
  • 44
  • 77
  • 3
    No it's not. Do you really want an acceptable CardColor to be CARDS_COUNT? – wheaties Jan 25 '11 at 16:28
  • 6
    @wheaties Actually, any integer it's an acceptable CardColor. That's why this question was asked in the first place. – vz0 Jan 25 '11 at 16:33
  • 1
    I think one problem is that `static_cast(100);` triggers undefined behaviour, and that means that in theory the returned value might even be HEARTS or DIAMONDS. See http://stackoverflow.com/a/33608071/2436175 – Antonio Nov 09 '15 at 14:26
  • @wheaties I have to agree with vz0. It's common practice where I work and a good way to handle consecutive enums. You can initialize containers using CARDS_COUNT, you can iterate over it with for loops using the value as terminator and you can do all sorts of sanity checks with it. The naming convention makes it clear that it is a special value that should not be used like the others. You just have to make sure that all enum values are consecutive. So it is a solution to OP's first example, but it does not generalize to his second case. – Cerno Mar 05 '19 at 09:59
  • The compiler chooses the size of the enum depending on the values. As long as the value fits in the representation, the value will be valid, even though it will be technically undefined behavior. It won't be different than for example assigning an int64_t to an int32_t. You'd better enable compiler warnings on downcasts. – vz0 Mar 06 '19 at 11:47
1

At the beginning - it's bad idea to do this.

But if you want, I suggest to hardcode integer values of enumeration:

enum CardColor { HEARTS = 10, DIAMONDS = 11, CLUBS = 12, SPADES = 13};

Then overload assigment operator:

CardColor operator = (int value)
{
    switch (value)
    {
        case 10:
            return HEARTS;
        // case for other values
        default:
            // throw an exception or something
    }
}
Nate Chandler
  • 4,533
  • 1
  • 23
  • 32
Goofy
  • 5,187
  • 5
  • 40
  • 56
  • while this technically does what the OP wants, it also completely invalidates the point of using an enum. @CashCow's answer is similar, but far better. – tenfour Jan 25 '11 at 16:54
0

Enumeration values may overlap or have holes; also, actual variables can be assigned zero, any value from the set or the bitwise OR of allowed values. So:

enum suites { hearts, diamonds, clubs, spades };

allows the values 0, 1, 2, 3;

enum suites { hearts = 1 << 0, diamonds = 1 << 1, clubs = 1 << 2, spades = 1 << 4 };

allows any value from 0 to 15.

If you use an enum to define bit values, it is usually a good idea to define (binary) operator&, operator|, operator&= and operator|=. If you don't do that, you will need an explicit cast whenever a value that is not in the set is generated, so the places where this happens can be easily spotted.

There are compilers that can warn if a number outside the allowed range is assigned, or if not either none, the first or all names have initializers attached to them (this is a violation of MISRA-C rules).

Simon Richter
  • 28,572
  • 1
  • 42
  • 64