Click here to Skip to main content
15,881,089 members
Articles / Programming Languages / C++

How to get the integer type of a C++ enum

Rate me:
Please Sign up or sign in to vote.
4.94/5 (17 votes)
20 Mar 2015MIT6 min read 63.6K   432   39   12
This article will show you how to determine the right integer type for a C++ enum in compile time with template meta-programming.

Introduction

I have used enumerators many times in C++ but sadly, these enums are not supported well. When you want to serialize an enum, you don't know what is the smallest integer type which can safely store the values of this enum. Of course, you can choose the biggest integer but it is not efficient, and in this case you don't handle signed/unsigned integers. I wrote a small class which can determine the right size of the enum at compile time. To do this, there are some restrictions:

  • The smallest value in the enum must be _MIN_VALUE
  • The highest value in the enum must be _MAX_VALUE
  • The enum must be nested into a struct or class (see the example)

If you meet these requirements, the smallest size can be determined correctly. Otherwise you will get a compile error.

I implemented the solution with the new C++14 standard. If you don't use the new C++ standard (C++0x/C++11 or above), then you can use the boost or the Loki library implementation which is compatible with the C++03 standard. You can choose which do you prefer. The implementation logics are very similar.

For more information about these libraries, visit the boost mpl or the Loki sites.

Using the code

It is very easy to use the class. Let's see!

C++
struct ResultLevel
{
    enum Value
    {
        _MIN_VALUE = -4,

        FATAL_ERROR,
        CRITICAL_ERROR,
        ERROR,
        OK,
        WARNING,

        _MAX_VALUE
    };
};

// ...

Integral<ResultLevel>::Type level = 0;
// the Type will be signed char (int8)
// _MIN_VALUE = -4 and _MAX_VALUE = 2

// ...

struct ASCII
{
    enum Value
    {
        _MIN_VALUE,

        MIN_CONTROL = _MIN_VALUE,
        MAX_CONTROL = 31,

        MIN_PRINTABLE = MAX_CONTROL + 1,
        MAX_PRINTABLE = 127,

        MIN_EXTENDED = MAX_PRINTABLE + 1,
        MAX_EXTENDED = 255,

        _MAX_VALUE = MAX_EXTENDED
    };
};

// ...

Integral<ASCII>::Type ascii = 0;
// the Type will be unsigned char (uint8)
// _MIN_VALUE = 0 and _MAX_VALUE = 255

In the above examples, the Type of the Integral will be the expected type. As you can see, it handles signed and unsigned integers, too, and of course, you can use any signed and unsigned integer (char, short, int, long, long long).

As you can see, there are some restrictions to use this class. First, you have to use _MIN_VALUE and _MAX_VALUE in your enums. _MIN_VALUE must be the smallest value (smaller than your first enum) and _MAX_VALUE must be the greatest enum value. It is necessary, because these two enums will help to determine the right integer type.

Each implementations (C++14, boost and Loki version) are in different namespaces:

  • Base::EnumeratorHelper::Loki::Integral<>
  • Base::EnumeratorHelper::Boost::Integral<>
  • Base::EnumeratorHelper::Std::Integral<>

Or, if you can define one of the following macro and you can simply access the class via Base::EnumeratorHelper::Integral<>:

  • INTEGRAL_LOKI
  • INTEGRAL_BOOST
  • INTEGRAL_STD

Now let's see the implementation!

The implementation

Integral

The main class which is called every time is the Integral class.

C++
template <typename _TEnumerator, typename..._TTypes>
struct Integral
{
protected:
    
    typedef typename TypeList<_TTypes...>::Iterator Types;
    
public:
    
    typedef typename FindIntegral<_TEnumerator, IntegralResult::cGetNextType, void, Types>::Type Type;
};

template <typename _TEnumerator>
struct Integral<_TEnumerator>
{
public:
    
    typedef typename Integral<_TEnumerator, uint8, int8, uint16, int16, uint32, int32, uint64, int64, uintmax, intmax>::Type Type;
    
};

The C++14 version is the simpliest, because with the power of the new standard (from C++11) we can use variadic template parameters.

In the boost and Loki implementation we using variadic sequence (boost::mpl::vector or Loki::TypeList) which intended to use as a variadic template parameter. Here is the boost version:

C++
template <typename _TEnumerator, typename _TTypes = void> struct Integral;

template <typename _TEnumerator>
struct Integral<_TEnumerator, void>
{
protected:

   typedef ::boost::mpl::vector<uint8, int8, uint16, int16, uint32, int32, uint64, int64, uintmax, intmax>::type Result;

public:

   typedef typename Integral<_TEnumerator, Result>::Type Type;
 
 };

template <typename _TEnumerator, typename _TTypes>
struct Integral
{
protected:

   typedef typename ::boost::mpl::deref< typename ::boost::mpl::begin< _TTypes >::type >::type TFirstType;

   static const bool isTypeListEmpty = !!::boost::is_same< typename ::boost::mpl::end< _TTypes >::type, typename ::boost::mpl::begin< _TTypes >::type >::value;
   static const int cSelectResult = isTypeListEmpty ? IntegralResult::cTypeListEmpty : IntegralResult::cGetNextType;

public:

   typedef typename FindIntegral<_TEnumerator, _TTypes, TFirstType, cSelectResult>::Type Type;

};

The boost mpl allows to refer to the first element in the type list event if the list is empty. In this case, it will be a void type. The Loki library will cause a compile error so the Loki's implementation using void type as _TFirstType in the implementation.

The Integral<>::Type contains the integer types in ascending order which will be checked when we are looking for the right type. It is important for the list to be in ascending order because the search is linear and will stop at the first match. intmax and uintmax are typedefs to intmax_t and uintmax_t which are part of the C99 ANSI standard. These types are the largest storable integers by the processor on the current system. For more information, see the section 7.18.1.5 in the standard or on Wiki. Integral<>::Type is a typedef and it uses the FindIntegral template which does the whole job. The cSelectResult determines which template will be specialized. In this case, if the type list is empty, we will get a compile error.

Error handling

The error handling in template meta-programming is very important, because the compile time template errors can be very confusing and it will be hard to decipher the root of the problem. If we handle every error, we get the following if the type list is empty in the Loki version:

error: variable 
  'Base::EnumeratorHelper::Loki::CompileTimeError::Integral_Type_List_Is_Empty i8' 
  has initializer but incomplete type

It is more readable than this:

findintegral.h:71: error: invalid use of incomplete 
  type 'struct Loki::TL::TypeAt<Loki::NullType, 0u>'
loki/typelist.h:121: error: declaration of 'struct Loki::TL::TypeAt<Loki::NullType, 0u>'

FindIntegral

FindIntegral does the whole job. The input parameters are the following:

  • _TEnumerator is the enumerator type (this is the ResultLevel enum in our example above)
  • _TTypes contains the possible types for the enum which is a variadic template in C++14. If you are using boost, then it is a boost::mpl::vector filled with integers, or a Loki TypeList in Loki library.
  • The _TFirstType integer type will be checked in the class. If it meets the expectations, the specialized template will be instantiated.
  • Result controls the instantiation of the template. The value can be one in the IntegralResult namespace. It represents a simple switch case statement at compile time. The selection depends on the value of the Result.

The new C++ standard supports variadic templates but there is no type list implementation as you see in the boost or loki library, so I implement a little helper which does this job.

C++
namespace TypeListHelper
{
    template <typename..._TTypes> struct Iterator;
    
    template <typename _TFirst, typename..._TTypes>
    struct Iterator<_TFirst, _TTypes...>
    {
        typedef _TFirst Type;
        typedef Iterator<_TTypes...> Next;
        
        static const bool cHasNext = true;
    };
    
    template <typename _TLast>
    struct Iterator<_TLast>
    {
        typedef _TLast Type;
        typedef void Next;
        
        static const bool cHasNext = false;
    };
}

template <typename..._TTypes>
struct TypeList
{
    typedef TypeListHelper::Iterator<_TTypes...> Iterator;
    
    static const size_t cSize = sizeof...(_TTypes);
};

The specialization of the FindIntegral template depends on the value of the Result. Here are the possible specializations:

  • cTypeListEmpty: This template specialization will be instantiated when there is no type in the list. This is an error case and will generate a compile time error.
  • cTypeNotFound: This is an error case too and will be instantiated when there is no matching integral type in the list.
  • cTypeFound: We found the right integral type.
  • cGetNextType: Search is in progress, "calling" while there is type in the list or type isn't found.
  • neither one: The value of the Result is invalid and generates a compile time error: Result_Parameter_Is_Invalid. This is an internal error.

The implementation of the FindIntegral are very similar in each version, so I show only one version, but you can see the others in the source code if you download it. This is the C++14 version:

C++
namespace IntegralResult
{
    static const int cTypeListEmpty = -2;
    static const int cTypeNotFound = -1;
    static const int cGetNextType = 0;
    static const int cTypeFound = 1;
}
    
namespace CompileTimeError
{
    namespace InternalError
    {
        class Result_Parameter_Is_Invalid;
    }
    class Integral_Type_Not_Found;
}
    
/**
 * Invalid input. Result is invalid with types.
 **/
template <typename _TEnumerator, int Result, typename _TFirstType, typename _TTypes>
struct FindIntegral
{
    typedef CompileTimeError::InternalError::Result_Parameter_Is_Invalid Type;
};
    
/**
 * The right integral type doesn't exist in the type list.
 **/
template <typename _TEnumerator, typename _TFirstType, typename _TTypes>
struct FindIntegral<_TEnumerator, IntegralResult::cTypeNotFound, _TFirstType, _TTypes>
{
    typedef CompileTimeError::Integral_Type_Not_Found Type;
};
    
/**
 * The right integral type has been found.
 **/
template <typename _TEnumerator, typename _TFirstType, typename _TTypes>
struct FindIntegral<_TEnumerator, IntegralResult::cTypeFound, _TFirstType, _TTypes>
{
    typedef _TFirstType Type;
};

template <typename _TEnumerator, typename _TFirstType, typename _TTypes >
struct FindIntegral<_TEnumerator, IntegralResult::cGetNextType, _TFirstType, _TTypes>
{
    typedef typename _TTypes::Type TFirstType;
        
    static const TFirstType cMinValue = ::Base::Type<TFirstType>::cMinValue;
    static const TFirstType cMaxValue = ::Base::Type<TFirstType>::cMaxValue;
        
    template <bool /* isTrue */, typename _TNull = void> struct IsInRange;
    template <typename _TNull>
    struct IsInRange<true, _TNull>
    {
        enum {
            value = (_TEnumerator::_MIN_VALUE >= static_cast<intmax>(cMinValue) &&
                    _TEnumerator::_MAX_VALUE <= static_cast<intmax>(cMaxValue))
        };
    };
    template <typename _TNull>
    struct IsInRange<false, _TNull>
    {
        enum {
            value = (_TEnumerator::_MIN_VALUE >= static_cast<uintmax>(cMinValue) &&
                     _TEnumerator::_MAX_VALUE <= static_cast<uintmax>(cMaxValue))
        };
    };
    static const bool isSigned = _TEnumerator::_MIN_VALUE < 0;
    static const bool isTypeFound = !!IsInRange<isSigned>::value;
    static const bool isEndOfList = (!isTypeFound) && (!_TTypes::cHasNext);
        
    static const int cSelectResult = 
        isEndOfList ?
            IntegralResult::cTypeNotFound :
        isTypeFound ?
            IntegralResult::cTypeFound :
        IntegralResult::cGetNextType;
        
    typedef typename FindIntegral<_TEnumerator, cSelectResult, TFirstType, typename _TTypes::Next >::Type Type;
};

There is a nested template class IsInRange<> and if it is true, then the right integer type has been found and the specialized FindIntegral<..., IntegralResult::cTypeFound> template will be instantiated and the search will be stopped. I used some static const variables as a helper to select the specialization of the template; cMinValue is the lowest value and cMaxValue is the largest value of _TFirstType. I had to implement a Type template to store the lowest and largest values for the integer types. It was necessary because I have to check these values at compile time for any integer type. The template is very simple:

C++
template <typename _TType> struct Type;
template <>
struct Type<uint8>
{
    static const uint8 cMinValue = 0;
    static const uint8 cMaxValue = UINT8_MAX;
    static const bool cIsSigned = false;
};
// and so on ...

As you can see, you must declare explicitly for every integer type; if you miss it, you'll get a compile error.

Let's go back to FindIntegral::isSigned is used for IsInRange and helps to check whether the type has been found or not. The isSigned static const boolean tells that the enumerator is signed or unsigned. The sign checking is necessary because we have to cast cMinValue and cMaxValue to the largest integer type to avoid compile warnings or any other problems, and it's now safe because we know the sign of _TEnumerator::_MIN_VALUE. The value of cSelectResult determines which FindIntegral template will be instantiated. The possible values of the cSelectResult mentioned earlier. There is one more restriction: the range of the integer type must be incremental, starting from the smallest type to the largest.

I implemented and attached the boost, Loki and C++14 versions too to the source. Check it out!

If you have any comment or idea, don't hesitate, let me know!

History 

  • 28 Dec 2011: Added boost support and error handling.
  • 11 Mar 2012: Added C++11 support with error handling.
  • 15 Mar 2013: Added Google test files. 
  • 19 Mar 2015: Refresh article, fix some mistakes and make the C++14 implementation as a default implementation.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Employed (other) http://www.otpbank.hu
Hungary Hungary

Comments and Discussions

 
GeneralNice try Pin
Lakamraju Raghuram11-Mar-12 17:34
Lakamraju Raghuram11-Mar-12 17:34 
GeneralRe: Nice try Pin
Gergely Mancz12-Mar-12 3:13
professionalGergely Mancz12-Mar-12 3:13 
QuestionThis is an area of c++ that is weak: your effort is a good thing Pin
Midnight48916-Jul-11 4:46
Midnight48916-Jul-11 4:46 
QuestionSimpler ways PinPopular
John R. Shaw13-Jul-11 9:06
John R. Shaw13-Jul-11 9:06 
AnswerRe: Simpler ways Pin
Gergely Mancz13-Jul-11 19:44
professionalGergely Mancz13-Jul-11 19:44 
GeneralRe: Simpler ways Pin
John R. Shaw14-Jul-11 13:48
John R. Shaw14-Jul-11 13:48 
SuggestionSL / TR1 instead of Loki Pin
Doc Lobster7-Jul-11 6:12
Doc Lobster7-Jul-11 6:12 
GeneralRe: SL / TR1 instead of Loki Pin
Gergely Mancz13-Jul-11 19:33
professionalGergely Mancz13-Jul-11 19:33 
SuggestionType class Template Pin
Doc Lobster4-Jul-11 18:37
Doc Lobster4-Jul-11 18:37 
GeneralRe: Type class Template Pin
Gergely Mancz4-Jul-11 22:23
professionalGergely Mancz4-Jul-11 22:23 
Yes, you're right this is part of the standard and I saw this numeric_limits class before I wrote my own. But there is one problem with it. The min() and max() are functions and only work in runtime, but I do everything in compile time and I can't use functions.

But your comment is valuable and I'll mention this in the article too.

Thanks,
MaG
AnswerRe: Type class Template PinPopular
Doc Lobster5-Jul-11 19:39
Doc Lobster5-Jul-11 19:39 
GeneralRe: Type class Template Pin
Gergely Mancz6-Jul-11 8:54
professionalGergely Mancz6-Jul-11 8:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.