This foundational ALib Module , comprises four "TMP type traits structs" aiming to enhance the use of C++ enumerations.
Those are:
Along with the type traits, corresponding operators, helper types and namespace functions are provided.
With scoped enums, neither arithmetical nor logical boolean operators that use enum elements as arguments, found their way into the language. This seems a little contradiction as enums have their underlying integral type, which even can be changed using a sort of inheritance syntax and with just a little static casting, these integral values are accessible.
Also, the other way round, an enum value in C++ can be initialized by passing arbitrary integral values (even values that no corrsponding element exist for). With enum declaration:
the following code compiles:
Therefore, if operators are needed, the common approaches are to either
std::is_enum
).The first approach imposes a lot of code duplication, the second has the disadvantage that it undermines the intention of the C++ language standard: The compiler would allow the operators even on enum types that are not considered to be "arithmetically defined".
Therefore, ALib takes a slightly more advanced approach: The operators defined by this ALib Module are available only for enum types which dispose about a specialization of either of two TMP structs.
This way, a subset of the provided operators can be "enabled" specifically for certain types and will not collide with similar operators found in other libraries or an ALib user's code base.
ALib differs between "arithmetical enums" and "bitwise enums" , which are introduced in the next two subsections.
TMP struct T_EnumIsArithmetical by default is derived from std::false_type
and is otherwise empty. If a specialization for an enumeration type derives from std::true_type
instead, the following set of operators become available to a scoped enumerations:
alib::enums
, in reality they are defined in the global namespace!using namespace alib::enums;is not needed in each source location that uses the operators.
For most operators two versions exist: one accepting an enum element for both operands and a second that accepts the underlying integral type of the enumeration for the right hand side operand.
While the specialization of TMP struct T_EnumIsArithmetical is a simple task, using provided macro ALIB_ENUMS_MAKE_ARITHMETICAL makes the code more readable.
If applied to the sample enum class given above as follows:
then the following code compiles:
TMP struct T_EnumIsBitwise by default is derived from std::false_type
and is otherwise empty. If a specialization for an enumeration type derives from std::true_type
instead, the following set of operators become available to a scoped enumerations:
alib::enums::bitwise
, while in fact they are defined in the global namespace!The term "bitwise" denotes that the elements of enums have numbers assigned which each represent one (or multiple) bit(s) in the underlying integral type.
As a sample, consider the following two enum types:
While type Fruits is an "ordinary" enumeration, type States is obviously of "bitwise nature". Obviously values of this enum represent "states of windows in a window manager", and for this, enum element values with multiple bits set might occur.
Therefore, in the sample, macro ALIB_ENUMS_MAKE_BITWISE is used to defined TMP struct T_EnumIsBitwise for the type.
With that, the following code snippet compiles:
In addition to these operators, namespace functions
become applicable. Please consult the functions' reference documentation for further information.
alib::enums::bitwise
, while in fact they are defined in namespace alib.Another "missing feature" of the C++ language in respect to scoped enums is the possibility to iterate over all enum elements defined in an enum type. The obvious reason why standard iterator functionality std::iterator_traits
and C++ range-based iterations are not applicable to enumerations is that enumerations are types and not containers or other iterable object instances.
Nevertheless it would still be nice if iteration was possible and for this to achieve, this tiny module provides a simple solution.
To have an easy mechanism for iterating over enum types, TMP struct T_EnumIsIterable may be specialized for a custom enum type that is not "sparsely" defined, which means that each element has an adjacent element with a difference of 1
of their assigned integral value. (All, but the last, of-course.)
The following gives a simple sample of a type that obviously meets the requirement:
A typical C++ code iterating over all enumerations would look like this:
The enums are stuffed in an array using a std::initializer_list
to be iterable. This is inefficient and error prone with changes of the enumeration definition.
As an alternative this module provides TMP struct T_EnumIsIterable which we specialize for enumeration Pets
using helper macro ALIB_ENUMS_MAKE_ITERABLE as follows:
With that in place, templated class EnumIterator becomes available for the enumeration type. The loop can be rewritten as follows:
In the previous section we used
to announce enum Pets
to ALib . Besides the enum type, the macro expects the "integral value of the last enum element plus 1".
You might have noticed that the term Pets::Snake + 1
usually is not valid C++ code, as we are adding an integral value to a scoped enum element.
The reason why this still compiles is that with a specialization of T_EnumIsIterable alib::enums::iterable;operator+;operator+<TEnum; int> "enums::iterable;operator+;operator+<TEnum; int>" and alib::enums::iterable;operator-;operator-<TEnum; int> "enums::iterable;operator-;operator-<TEnum; int>" become available.
In the sample discussed, Pets::Snake + 1
was used as the "end value" of an iteration. This is error prone in the respect that if the enumeration type gets extended, our macro invocation might be changes, as Pets::Snake
then is not the last in the list.
A way out, is to add a "stopper" element to the enumeration and name it special, e.g. in upper case "END_OF_ITERABLE_ENUM"
. It is then rather unlikely, that some programmer would put a new element behind this one. Furthermore, the macro statement would never needed to be changed:
ALIB_ENUMS_MAKE_ITERABLE(MyEnum, MyEnum::END_OF_ITERABLE_ENUM )
A next advantage is that within the enum declaration itself it becomes obvious that this is an iterable enum type and somewhere in the gloabl namespace of the same header file the specialization for T_EnumIsIterable will be found. Of-course, the drawback is that an enum element is presented to the C++ compiler that is not an element like the other ones.
So far, all our samples used macro
to specialize this struct. In fact, this macro is just a shortcut to macro
passing TEnum(0)
as a start value.
This lifts the restriction of having integral 0
underlying the first enum element.
The std::iterator_traits
returned with methods EnumIterator::begin and EnumIterator::begin implements the standard library concept of RandomAccessIterator and with this offers various operators, including subscript operator
[].
Iteration works well, if an TMP struct T_EnumIsBitwise is specialized in parallel to T_EnumIsIterable . The restriction described in 3.1 TMP Struct T_EnumIsIterable, namely that enum types must not be "sparsely" defined, in this case means that, every next enum element has the next bit set, hence its internal value is doubled with each next element.
Macro ALIB_ENUMS_MAKE_ITERABLE, chooses integral value 1
as a start element. Again, if ALIB_ENUMS_MAKE_ITERABLE_BEGIN_END is used, iteration might start on higher values.
Class EnumIterator is empty in respect to fields. Created on the stack there is no performance penalty. The same is true for the internal iterator type, which is returned with class EnumIterator::begin and class EnumIterator::end . This iterator class uses an TEnum element as its only field member. While the code with operators, casting and conversion seems quite complex, at least with compiler optimizations turned on (release builds), the loop will perform the same as an integral while loop:
int i= 0; int stop= 5; while( i++ < stop) { ... }
Often, a subset of enumeration elements need to be stored in a set. If the only purpose for the enumeration is to do exactly this, the solution is to define an enum as "bitwise", as discussed in previous chapter 2.2 Bitwise Operators. With that, a combination of elements can easily be be stored and tested in an integral value. If however, in contrast, the enumeration still "needs" to have a standard sequential numbering, then for the sake of storing permutations of elements, a "bitset", for example std::bitset
which holds one bit per possible element is the way to go.
ALib provides a powerful alternative to standard type std::bitset
, which makes the work with enumerations very convenient.
Let us come back to our previous sample of enum Pets:
As soon as we include additional header:
type definition EnumBitSet becomes available. This type simply fills out the right template parameters for target type TBitSet .
Those are:
0
, but the implementation also allows other ranges like 1000..1050, resulting in a bit set with a capacity of 50.With this type, we can now define a bit set as follows:
And easily fill it with:
Because TBitSet provides efficient bidirectional iterator types that deliver just the bits that are set, a simple loop like this can be implemented:
We flip the set and loop again:
The output of this code is:
For the full documentation of the features of type EnumBitSet, consult the reference documentation of underlying class TBitSet and keep in mind, that wherever template type TInterface is mentioned, a C++ enum element can be directly provided. The only prerequisite is that preprocessor macro ALIB_ENUMS_MAKE_ITERABLE is applied to the enum class.
We have seen in the previous sections of this manual that C++ enumeration types are - by language design - quite limited in their functionality. So far, we have added various operators and an iterator type, which all become activated using template meta programming (TMP) and on the user's side by specializing simple corresponding type traits structs.
Probably the most powerful feature of this ALib Module is provided with the concept ALib Enum Records, which again is enabled for a custom enumeration type by specializing another struct, namely T_EnumRecords .
The features achieved with this are:
While technically the implementation of ALib Enum Records is very simple and their use is likewise very straightforward, to leverage their potential, it is important to understand a design pattern of their use. A step by step sample of this design pattern is explained in chapter 4.5 A Design Pattern For Using Enum Records.
For now, lets start with the simple things.
In chapter 2.2 Bitwise Operators a simple enumeration type Fruits was given. If this was not C++ but a "higher level" programming language, we could print out the name of the enum elements easily.
This is for example Java code:
For a typical C++ programmer, the purpose of having a language feature that provides run-time information on "source code elements" might be very questionable, but still let's have a try to do something similar with ALib Enum Records.
Here is our enumeration:
It is only three simple steps to be done.
First we need a suitable "record type" to store the element names:
Now we specialize TMP struct T_EnumRecords for enum Fruits. The only single entity in this struct is given with using
-statement T_EnumRecords::Type , which defaults to void
in the non-specialized version. With the specialization this is set to our record type ERFruits.
The struct is included with:
The easiest way to do the specialization is by using macro ALIB_ENUMS_ASSIGN_RECORD as follows:
We are almost done. The final step of preparation is to define the data records. This is to be done when bootstrapping a software.
To define enum records, several overloads of static method Bootstrap are provided by class EnumRecords . While the type and the method's declarations are already available with the inclusion of alib/enums/records.hpp , the definition of the set of Bootstrap methods is only given with including alib/enums/recordbootstrap.hpp :
This is a precaution to assure that the methods are used only with bootstrap code, for example in implementations of abstract function Camp::bootstrap .
The simplest version of Bootstrap accepts one enumeration element along with variadic template parameters used to construct its record. This has to be invoked for each element:
To avoid the multiple invocations, a first overload of Bootstrap exists that accepts a std::initializer_list
, which is more performant and has a shorter footprint:
That's it, the enum records are ready to be used! While later in this manual even more efficient ways of initializing enum records will be introduced, the next two sections are about using the data.
To access the three enum records of type Fruits, two possibilities are offered.
In the Java sample above, method printFruit was provided as:
Here is now the corresponding C++ code:
This is using namespace function GetRecord passing the given enum element. The function returns a reference to the record defined during bootstrap.
Invoked like this:
produces the following output:
Apple
We're done! We have mimicked the functionality of enums that is built into the Java language!
But in C++ things are always a little more complicated. The following is a valid invocation of our method:
While no "named" enum element is given in the enumeration, still the language allows to "construct" elements with arbitrary numbers. As there is no record assigned to element 42
, the invocation would generate a runtime error, because method GetRecord returned a null-reference. In debug-compilations, function GetRecord raises an ALib assertion in this case.
Therefore, in situations where a code does not "know" if undefined enum elements are passed, the way out of this is to use sibling function TryRecord . This returns a pointer, and does not assert, but rather returns nullptr
if a record is not found.
The implementation then looks like this:
and invoked as above, it produces:
Fruits(42)
These two namespace functions are all that is given to retrieve specific enumeration records. It is as simple as this!
The second method to access enum records is to iterate over all defined records. Iteration is limited to forward iteration and the order of records follows the order of their definition during bootstrap.
Iteration is performed using static templated class EnumRecords which provides static begin() and end() methods. When using a range-based for(:)-loop, C++ requires an iterable object. For this, the static class has a default constructor, which is needed to be called.
Staying with our sample, a simple loop looks like this:
and produces the following output:
C++ range-based for(:)-loops dereference the iterators returned, and this gives us a reference to the enum records. Unfortunately, the enumeration's element is not accessible this way. Therefore, if needed, a standard for(;;)-loop has to be used.
As an example, let us "parse" an enum value from a given string:
Inner iterator type EnumRecords::ForwardIterator offers methods Enum and Integral which return the enum element, respectively its underlying integral value.
The following code compiles and executes without an assertion:
The author of this manual anticipates that an experienced C++ programmer might not be impressed much of the functionality that ALib Enum Records offer and that - by reading so far - its use might be questionable.
The answer, why we think this concept is very valuable is given only in later chapter 4.5 A Design Pattern For Using Enum Records and a curious reader might "fast forward" to that chapter.
Meanwhile, we have some details to explain.
What was sampled in the previous sections, namely writing out the C++ element names of enum class Fruits and parsing it back from a string, could be named serialization and de-serialization of enum elements.
Because this is a frequent requirement (and therefore even a built-in feature with languages like Java) functionality for this is built-into this module, ready to be used.
The clue to this feature is predefined enum record type ERSerializable . Besides field EnumElementName , this record has a second member with MinimumRecognitionLength . If this is set to a value greater than 0
it determines the minimum characters of the element name needed to give when parsing.
Implementing the Fruits-sample is now only two steps, because the first step, defining a record type can be omitted:
Step 1: Assigning record type ERSerializable to enum class Fruits:
Step 2: Defining the records (during bootstrap):
Because all of the element's names start with a different character, we allow to recognize each with just 1
minimum character specified.
As soon as:
is included, elements of that enum become appendable to instances of type AString :
As with every type that is appendable to AString instances, with inclusion of header file:
we can use std::ostream::operator<<
likewise:
For parsing enum elements back from strings, templated namespace function Parse is given:
The built-in facilities to serialize and deserialize enumeration element are:
By language definition, inheritance hierarchies are not available for C++ enumeration types. With assigning a record type to enumeration types, the inheritance hierarchy of the record can be used!. Questions like "Is enum type X inherited from enum type Y?" can be decided at compile time. This "side effect" simply emerges of from the concept "ALib Enum Records".
The most relevant use case that leverages this inheritance relationship is to have functions accept enum elements as arguments only if they are of a certain "derived enum type".
A quick sample demonstrates this. If the following is given:
then, function acceptBaseOrDerived can be invoked with enums of type Base or Derived, but not with elements of type Anything:
This feature is a foundation for a powerful design pattern that is introduced in later chapter 4.5 A Design Pattern For Using Enum Records and which is used with various other ALib Modules .
In respect to what we have seen so far, it is notable that the built-in serialization/de-serialization functionality introduced in the previous section ( given with T_Append<...> , Parse , ParseBitwise , ParseEnumOrTypeBool ) are applicable not only to enum types associated with records of type ERSerializable but also with any custom record type that derives from this!
Consequently, almost all enumeration records found within other ALib Modules are derived from ERSerializable and for most custom record types it appropriate to do.
It is allowed to define multiple enum records for a single element of an enumeration. If done, then
Again a sample helps to quickly understand the rationale and a use case for multiple records. Built-in ALib enumeration type Bool has two elements; Bool::False and Bool::True . The data record definition performed during bootstrap is found in this module's initialization function Bootstrap :
The implementation of appending an element of a serializable enum type T_Append uses TryRecord and thus receives the first given names for the elements, namely "False"
and "True"
. In contrast, method Parse iterates over all records and tries to recognize a name associated to an element. With the multiple sets given, alternatives to "False"
are "0"
, "No"
, "Off"
and "-"
.
If for example a boolean value should be parsed from an INI-file, all of these values are recognized.
With the exception of given names "On"
, "Off"
and "OK"
, all names start with a different character and a value of 1
is given for field ERSerializable::MinimumRecognitionLength . This allows string "F"
to be parsed as Bool::False. For the names starting with 'O'
this value is 2
to avoid ambiguities while parsing.
Especially with parsing elements, sometimes the order of the records is important. This should quickly be demonstrated with the record definitions of built-in enum typeContainerOp :
Here, element name "Get"
and "GetCreate"
share the same first letter. Nevertheless, element "Get"
is allowed to be recognized by only one character. To avoid ambiguities, only the minimum recognitions length of "GetCreate"
was increased to 4
. If this is done, then the longer name has to be placed first in the list, otherwise even the full string "GetCreate"
was recognized as "Get"
.
The way of appending enum elements to AString instances is implemented with two different methods, depending on whether T_EnumIsBitwise is specialized for an enum type or not (see chapter 2.2 Bitwise Operators). If it is, instead of just trying to receive a defined record for an enum value, the bitwise version acknowledges multiple definitions in a tricky and convenient way. Details and a sample code for this is given with T_Append<TEnumBitwise,TChar> .
As stated in the previous sections, ALib Enum Records are considered static data, which is "manifested" with three design decisions:
This is very well in alignment with common string resources that may either reside in the data segment of a software or that are externalized to be maintainable without recompiling the software (translations, core-configuration, etc.).
ALib provides module ALib BaseCamp which implements the concept of Externalized String Resources and, as shown in this chapter, both modules go along very well.
So far, in this manual the initialization of enum records has been performed using methods
(See chapter 4.1.3 Step 3/3: Initializing The Data for sample code).
A next method offered accepts a string and two delimiter characters:
It allows to parse a single record or an array of records from a string. In alignment with the constrains of enum records, the string provided to this method has to be static itself, for example a C++ string literal. This restriction, allows that no copies of single string type fields of records have to be made. The buffers of string members of records, after parsing, simply point to the corresponding sub-string of string given!
Parsing of custom record types has to be supported by custom code implementing parameterless method Parse, which "by contract" of the TMP code has to be available. Details are given with non-existing, pure documentation type EnumRecordPrototype . Method Parse is parameterless, because all parsing information (current remaining input string and delimiters) are accessible through 100% static helper type EnumRecordParser . The latter provides convenient methods to parse fields of string, integral, floating point, character and enum type.
With this knowledge, and the fact that the arguments defining the inner and outer delimiter characters default to ','
, the definition of the records of our sample enum Fruit changes now from:
to
With the inclusion of module ALib BaseCamp in an ALib Distribution , the strings used to define enum records should be resourced. A next overload of method Bootstrap supports this:
While its use is straight forward, it has a specific feature, which is about separating the each record's definition string into an indexed list of separated resource strings.
Please consult the reference documentation of this method for further information. This feature is likewise available for the upcoming two further methods.
Module ALib BaseCamp provides tool TMP struct T_Resourced to announce resource information for a type, aiming to have this information available in independent places. If a specialization of that struct is given for an enumeration type, overloaded method
becomes available. The fact that this overload reads the information from T_Resourced, becomes obvious from the reduced argument list.
An important advantage of using TMP struct T_Resourced when used with ALib Enum Record definition strings, is that code that reads and uses a record may string members of the records in turn as resource names, which it then loads when needed. For this, it ignores method T_Resourced::Name and uses the name given in that field instead.
If so, the "contract" that custom enums that are passed to such code have to fulfill then may include that a specialization for T_Resourced is given.
The final overload of method is given with:
This method should be used when a software uses class Camp to organize bootstrapping, resource management, configuration data, etc., just the same as any full ALib Module does.
If so, this method is preferred over all others versions, except for the cases where the use of T_Resourced is mandatory or otherwise superior, as described in the previous section.
Further information is given with the programmer's manual of module ALib BaseCamp .
A comprehensive sample of using ALib resources placed in a custom module is provided with the tutorial of ALib Module 'CLI'. The sample code provided there, can be easily used as a jump start into an own project that creates a custom ALib module and leverages the resource features provided.
The previous chapters 4.1 - 4.2 provided a step by step tutorial-style guide into the concept of ALib Enum Records. The code behind this small ALib Module is easy but still needs a little explanation - because it is as much a design concept as it is a library.
This chapter wants to give further thoughts about this concept.
A reader might nevertheless think:
std::vector
or std::unordered_map
instead, without the need of using a library for this.And right: Instead of talking about "Enum Records" this library could talk about
From this angle of view, the only difference this concept brings is:
typeid
). These two small points constitute the real value ALib Enum Records and their reason for existing.
The single important consequence of this design was mentioned already in section 4.3.2 Enum Inheritance Relationships. The inheritance relationship simply emerges from the inheritance relationship of the assigned record types.
The circle closes in the moment that two different code unit start interacting: Unit A offers a service that unit B wants to use. In some occasions, ALib Enum Records can be a perfect concept to define a very elegant interface for B into A.
With that, the concept of enum records, transitions in to a "programming paradigm" or "design pattern".
Implementations of this pattern are found several times with other ALib Modules . We want to quickly sample one implementation found with module ALib Configuration :
This module manages external configuration data. The data is organized in "variables" that define a category and a name and carry, besides a list of values, some meta information like a description text for end users.
The interface into this module is designed and used as follows:
This is all that needs to be done. The advantages are
Another prominent sample is found with class Exception . Furthermore, module ALib CLI makes really heavy use of this paradigm.
As this module's name indicates, the four major features introduced in this manual are all about adding features in the area of C++ enumeration types
A fifth valuable tool type that supports working with enumerations is given with class boxing::Enum . The type is located in module ALib Boxing , because it leverages that module's features and in fact it is just a small extension of the module's central class Box itself.
In short, class Enum is a small wrapper that can be constructed with enumeration elements of arbitrary type. Along with the underlying integral value of an enum element, run-time type-information is stored.
This mechanism allows to have custom methods with arguments that in turn accept arbitrary enumeration elements and that defer actions with these arguments for later type.
With methods Enum::GetRecord and Enum::TryRecord , such postponement is also offered for the concept of ALib Enum Records.