Search code examples
c++c++11coding-style

How to restrict conversion from int to enum class?


If I have a enum class like this:

enum class Weekday {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday
}

and I have a conversion from int to Weekday:

Weekday day = static_cast<int>(value);

To check that the value provided for conversion is valid, I should have the code:

if (value != 0 && value != 1 && value !=2 && value !=3 && value != 4 && value != 5 && value != 6) { 
  do_conversion(value); 
}

It's so ugly. Is there any good way to do this?


Solution

  • This is probably over-complicating things on the surface, but it generalizes nicely and the template voodoo can be hidden in a header away from sight.

    #include <stdexcept>
    template <class TYPE>
    TYPE do_conversion(int value)
    {
        if (value >= static_cast<int>(TYPE::First) &&
            value < static_cast<int>(TYPE::Last))
        {
            return static_cast<TYPE>(value);
        }
        throw std::out_of_range("Inv value");
    }
    

    All of the conversion is moved to a function to keep the noise down. The valid range is abstracted with extra enumerated values First and Last. If the given value is in the range (First, Last], the value is assigned. If not, throw exception. Exception might not be the right thing to do depending on the frequency of invalid inputs, hardly the exception if bad input is common, but for here it helps keep the example simple.

    Now invoking is a simple matter of

    enum class Weekday
    {
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday,
        Sunday,
        Last,           // added
        First = Monday  // added
    };
    

    and later

    Weekday day = do_conversion<Weekday>(value);
    

    All of the noise from the conversion is out of the way of whatever needed the conversion so whoever is reading or writing that bit of code can get on with their job.

    But what if you use it on an enum without First and Last? The compiler messages can get bizarre, so it's a good idea to help the user out a bit. The following has been looted from How to detect whether there is a specific member variable in class? to make this a bit easier to use. Definitely take the time to read the answer to grok what's going on. I haven't come up with a decent way to combine HasFirst and HasLast. Please drop a comment and let me know if you have one.

    #include <stdexcept>
    #include <type_traits>
    template <typename T, typename = int>
    struct HasFirst : std::false_type { };
    
    template <typename T>
    struct HasFirst <T, decltype((void) T::First, 0)> : std::true_type { };
    
    template <typename T, typename = int>
    struct HasLast : std::false_type { };
    
    template <typename T>
    struct HasLast <T, decltype((void) T::Last, 0)> : std::true_type { };
    
    template <class TYPE>
    TYPE do_conversion(int value)
    {
        static_assert(HasFirst<TYPE>::value, "enum missing First");
        static_assert(HasLast<TYPE>::value, "enum missing Last");
        if (value >= static_cast<int>(TYPE::First) &&
            value < static_cast<int>(TYPE::Last))
        {
            return static_cast<TYPE>(value);
        }
        throw std::out_of_range("Inv value");
    }
    

    Now if the enum is missing the all-important meta values, the compiler can tell them with a simple error message.

    Test case