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






4.94/5 (16 votes)
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!
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.
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:
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 theResultLevel
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 aboost::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 theIntegralResult
namespace. It represents a simpleswitch case
statement at compile time. The selection depends on the value of theResult
.
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 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:
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:
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.