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;
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 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;
}
template <typename _TEnumerator, typename _TTypes,
typename _TFirstType, int Result>
struct FindIntegral
{
typedef CompileTimeError::InternalError::Result_Parameter_Is_Invalid Type;
};
template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType,
IntegralResult::cTypeListEmpty>
{
typedef CompileTimeError::Integral_Type_List_Is_Empty Type;
};
template <typename _TEnumerator, typename _TTypes, typename _TFirstType>
struct FindIntegral<_TEnumerator, _TTypes, _TFirstType,
IntegralResult::cTypeNotFound>
{
typedef CompileTimeError::Integral_Type_Not_Found Type;
};
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 , 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;
};
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.