Click here to Skip to main content
Click here to Skip to main content

How to determine the integer type for a C++ enum

, 15 Mar 2013
Rate this:
Please Sign up or sign in to vote.
This article will show you how to determine the right integer type for a C++ enum.

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 Loki library, boost and with the new C++11 standard. You can choose which you prefer. The implementation logics are very similar.

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

Using the code

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

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

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).

Each implementations (Loki, boost and C++11 version) are in different namespaces (Base::EnumeratorHelper::Loki::Integral<>, Base::EnumeratorHelper::Boost::Integral<>, Base::EnumeratorHelper::Std::Integral<>). If you define INTEGRAL_LOKI, INTEGRAL_BOOST or INTEGRAL_STD then you can use Base::EnumeratorHelper::Integral<> to access the class.

The implementation

Integral

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

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

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

template <typename _TEnumerator, typename _TTypes>
struct Integral
{
protected:
    
    static const bool isTypeListEmpty = (0 == ::Loki::TL::Length<_TTypes>::value);
    static const int cSelectResult = isTypeListEmpty ? 
           IntegralResult::cTypeListEmpty : IntegralResult::cGetNextType;
    
public:
    
    typedef typename FindIntegral<_TEnumerator, _TTypes, void, cSelectResult>::Type Type;
};

Integral<>::Types contains integers in ascending order which will be checked when we are looking for the right integer 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.

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:

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>'

The boost version is very similar.

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. That is the reason for using the Loki implementation void type as _TFirstType.

In the C++11 version I implemented a wrapper to enclose the variadic template parameters into a struct. This helper struct helps to use the same implementation logic as in the Loki or boost version.

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;
    
};

FindIntegral

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

  • _TEnumerator is the enumerator type.
  • _TTypes is a Loki TypeList, or if you are using boost, then it is a boost::mpl::vector filled with integers.
  • 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.

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 sililar in each version, so I only one version, but you can see it in the source code if you download it.

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_List_Is_Empty;
    class Integral_Type_Not_Found;
}

/**
 * Invalid input. Result is invalid.
 **/
template <typename _TEnumerator, typename _TTypes, 
         typename _TFirstType, int Result>
struct FindIntegral
{
    typedef CompileTimeError::InternalError::Result_Parameter_Is_Invalid Type;
};

/**
 * The Type list is empty.
 **/
template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType, 
       IntegralResult::cTypeListEmpty>
{
    typedef CompileTimeError::Integral_Type_List_Is_Empty Type;
};

/**
 * The right integral type doesn't exist in the type list.
 **/
template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType, 
       IntegralResult::cTypeNotFound>
{
    typedef CompileTimeError::Integral_Type_Not_Found Type;
};

/**
 * The right integral type has been found.
 **/
template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType, IntegralResult::cTypeFound>
{
    typedef _TFirstType Type;
};

template <typename _TEnumerator, typename _TTypes>
struct FindIntegral<_TEnumerator, _TTypes, void, IntegralResult::cGetNextType>
{
    typedef typename ::Loki::TL::TypeAt<_TTypes, 0>::Result TFirstType;
    
    typedef typename FindIntegral<_TEnumerator, _TTypes, TFirstType, 
            IntegralResult::cGetNextType>::Type Type;
};

template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType, IntegralResult::cGetNextType>
{
    typedef typename ::Loki::TL::TypeAt<_TTypes, 0>::Result 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) && 
          (1 >= ::Loki::TL::Length<_TTypes>::value);
    
    static const int cSelectResult = 
        isEndOfList ?
            IntegralResult::cTypeNotFound :
        isTypeFound ?
            IntegralResult::cTypeFound :
        IntegralResult::cGetNextType;
    
    typedef typename FindIntegral<_TEnumerator, typename ::Loki::TL::Erase<_TTypes, 
            TFirstType>::Result, TFirstType, cSelectResult >::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:

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++11 versions too to the source. Check it out!

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. 

License

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

About the Author

Gergely Mancz
http://www.ipsystems.hu
Hungary Hungary

Comments and Discussions

 
GeneralNice try PinmemberLakamraju Raghuram11-Mar-12 17:34 
GeneralRe: Nice try PinmemberGergely Mancz12-Mar-12 3:13 
QuestionThis is an area of c++ that is weak: your effort is a good thing PinmemberD N Harris16-Jul-11 4:46 
QuestionSimpler ways PinmemberJohn R. Shaw13-Jul-11 9:06 
AnswerRe: Simpler ways PinmemberGergely Mancz13-Jul-11 19:44 
GeneralRe: Simpler ways PinmemberJohn R. Shaw14-Jul-11 13:48 
SuggestionSL / TR1 instead of Loki PinmemberDoc Lobster7-Jul-11 6:12 
Hi Gergely, I've got another idea. Have you tried to use the tuple class template provided by TR1 / C++0x instead of the Loki typelist? I suppose you could address more people that way - including me Big Grin | :-D
GeneralRe: SL / TR1 instead of Loki PinmemberGergely Mancz13-Jul-11 19:33 
SuggestionType class Template PinmemberDoc Lobster4-Jul-11 18:37 
GeneralRe: Type class Template PinmemberGergely Mancz4-Jul-11 22:23 
AnswerRe: Type class Template PinmemberDoc Lobster5-Jul-11 19:39 
GeneralRe: Type class Template PinmemberGergely Mancz6-Jul-11 8:54 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 15 Mar 2013
Article Copyright 2011 by Gergely Mancz
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid