How to determine the integer type for a C++ enum?
Introduction
I use 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 the signed/unsigned integers.
I wrote a small class which can determine the right size of the enum in compile time. To do this there are some restrictions:
- The smallest value in the enum must be the
_MIN_VALUE
- The highest value in the enum must be the
_MAX_VALUE
- The enum must be nested into a struct, class or namespace (see the example)
In this article I used the Loki library. For more information, visit the site
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 see, it handles the signed and unsigned integers, too and of course you can use every signed and unsigned integers (char, short, int, long, long long).
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, typename _TTypes>
struct Integral
{
protected:
typedef typename _TTypes::Result Types;
typedef typename Loki::TL::TypeAt<Types, 0>::Result TFirstType;
public:
typedef typename FindIntegral<_TEnumerator, TFirstType, Types, false>::Type Type;
};
template <typename _TEnumerator>
struct Integral<void>
{
protected:
typedef Loki::TL::MakeTypelist<uint8, int8, uint16, int16, uint32, int32, uint64, int64, uintmax, intmax>::Result Types;
public:
typedef typename Integral<_TEnumerator, Types>::Type Type;
};
Integral<>::Types
contains the integers in ascendent order which will be checked when we are looking for the right integer type. It is important for the list to be in ascendent order because the search is linear and will stop on the first match. The
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<>::TFirstType
is just a helper typedef. This is the current type checked by FindIntegral
.Integral<>::Type
is a typedef and it uses the FindIntegral
template which does the whole job. The other two typedefs above are the helpers for this.
FindIntegral
The FindIntegral
consists of two parts. One of them implements the search and the other one (specialized template) is used when the type has been found.
It requires 4 template parameters:
_TEnumerator
is the enumerator type._TFirstType
integer type will be checked in the class. If it meets the expectations, the specialized template will be instantiated._TTypes
is a Loki TypeList filled with integers.isFound
controls the instatiation of the template. If it is true, we have found the right integer and the specialized template will be instantiated and the search will be finished.
template <typename _TEnumerator, typename _TFirstType, typename _TTypes, bool isFound> struct FindIntegral; // declaration
template <typename _TEnumerator, typename _TFirstType, typename _TTypes>
struct FindIntegral<_TEnumerator, _TFirstType, _TTypes, true> // we have found the type
{
typedef _TFirstType Type;
};
template <typename _TEnumerator, typename _TFirstType, typename _TTypes, bool isFound>
struct FindIntegral
{
typedef _TFirstType TFirstType;
typedef typename Loki::TL::TypeAt<_TTypes, 0>::Result TNextType;
static const _TFirstType cMinValue = Base::Type<_TFirstType>::cMinValue;
static const _TFirstType cMaxValue = Base::Type<_TFirstType>::cMaxValue;
static const bool isSigned = _TEnumerator::_MIN_VALUE < 0;
template <bool /* isSigned */, typename _TNull = void> struct Check;
template <typename _TNull>
struct Check<true, _TNull>
{
static const bool isInRange = (_TEnumerator::_MIN_VALUE >= static_cast<intmax>(cMinValue) &&
_TEnumerator::_MAX_VALUE <= static_cast<intmax>(cMaxValue));
};
template <typename _TNull>
struct Check<false, _TNull>
{
static const bool isInRange = (_TEnumerator::_MIN_VALUE >= static_cast<uintmax>(cMinValue) &&
_TEnumerator::_MAX_VALUE <= static_cast<uintmax>(cMaxValue));
};
typedef typename FindIntegral<_TEnumerator, TNextType,
typename Loki::TL::Erase<_TTypes, TFirstType>::Result,
Check<isSigned>::isInRange >::Type Type;
};
There is a nested template class
Check<>
which checks that the enum value can be stored safely in the current integer type _TFirstType
. If isInRange
is true, then the right interger type has been found and the specialized FindIntegral
template will be instantiated and the search will be stopped.I used some static const variables as a helper to check the range;
cMinValue
is the lowest value and the cMaxValue
is the lagest value of the _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 in compile time for any integer types. 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 see, you must declare explicitly for every integer type; if you miss it, you'll get compile error.
Let's go back to
FindIntegral
; the isSigned
used for Check
and determines which specialized template will be instantiated. The _TEnumerator::_MIN_VALUE
tells that the enumerator is signed or unsigned. The sign checking is necessary because we have to cast the
cMinValue
and cMaxValue
to the largest integer type to avoid compile warnings or any other problem and it's now safe, because we know the sign of the _TEnumerator::_MIN_VALUE
.The value of the
Check::isInRange
determines which FindIntegral
template will be instantiated. If the type has been found, the "iteration" stops and the Type
typedef contains the right type.One more restriction: the order of the integer type must be incremental, started from the smallest type to the largest.