ALib C++ Library
Library Version: 2412 R0
Documentation generated by doxygen
Loading...
Searching...
No Matches
ALib Module Enums - Programmer's Manual

1. Introduction

This foundational ALib Module, comprises four "TMP type traits structs" aiming to enhance the use of C++ enumerations.
Those are:

  1. T_EnumIsArithmetical
    Enables simple math operators on elements of enumerations.
  2. T_EnumIsBitwise
    Enables bitwise operators on elements of enumerations with bitwise numbering.
  3. T_EnumIsIterable
    Specifies the lower and upper bounds of enum types with consecutive numbering of elements.
  4. T_EnumRecords
    Assigns a "record type" to an enumeration and allows retrieving a static definition of such record for it's elements.

Along with the type traits, corresponding operators, helper-types and namespace functions are provided.

2. Arithmetical and Bitwise Enums

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:

enum class MyEnum
{
One = 1,
Two = 2,
Five = 5,
};

the following code compiles:

auto myElement_1 = MyEnum::One;
auto myElement_2 = MyEnum::Two;
auto myElement_3 = MyEnum(3); // compiles well
auto myElement_42= MyEnum(42); // compiles well

Therefore, if operators are needed, the common approaches are to either

  • define operators for each scoped enum type that need them, or to
  • provide a set of operator functions that accept any enum types (restricted using 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.

2.1 Standard Arithmetical Operators

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:

Attention
While in this documentation the operators are appearing under namespace alib::enums, in reality they are defined in the global namespace!
Faking the documentation namespace was done to have the documentation of the operator functions collected in this ALib Module's namespace.
By using the global namespace, a using statement like
   using namespace alib::enums;
is not needed in each source location that uses the operators.
Note that for with the template meta programming used, this does not 'clutter' the global namespace in an harmful way and the operators do not interfere with any existing user code, that defines similar operators on enumerations.

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:

auto myElement_3 = MyEnum::One + MyEnum::Two;
auto myElement_42 = MyEnum::Five + 37;
auto myElement_43 = myElement_42++;
myElement_42-= 1;

2.2 Bitwise Operators

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:

Attention
Likewise with the operators introduced in the previous section, this documentation "fakes" the operators into namespace alib::enums::bitwise, while in fact they are defined in the global namespace!
See note in the previous section for details.

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:

enum class Fruits
{
Apple,
Orange,
Banana,
};
namespace WindowManager
{
enum class States
{
HorizontallyMaximized = (1 << 0),
VerticallyMaximized = (1 << 1),
Hidden = (1 << 2),
};
}
ALIB_ENUMS_MAKE_BITWISE( WindowManager::States )

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:

auto newState= ( currentState + ( States::HorizontallyMaximized
+ States::VerticallyMaximized ) )
& ~States::Hidden;

In addition to these operators, namespace functions

become applicable. Please consult the functions' reference documentation for further information.

Attention
Likewise with the operators, this documentation "fakes" these functions into namespace alib::enums::bitwise, while in fact they are defined in namespace alib.
See also
A different approach to allow bitwise operations on "ordinary" enums (like Fruits in the sample above) is introduced in later chapter 3.5 Using Class TBitSet with Iterable Enums.

3. Iterable Enums

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.

3.1 TMP Struct T_EnumIsIterable

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

Attention
ALib is not able to check if this requirement is met for a given type. It is the user's responsibility to ensure this and specialize this TMP struct only for such types.

The following gives a simple sample of a type that obviously meets the requirement:

enum class Pets
{
Cat,
Dog,
Bird,
Snake,
};

A typical C++ code iterating over all enumerations would look like this:

// loop over pets
for( auto element : { Pets::Cat, Pets::Dog, Pets::Bird, Pets::Snake } )
{
// do something...
cout << UnderlyingIntegral(element) << endl;
}

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:

ALIB_ENUMS_MAKE_ITERABLE(Pets, Pets::Snake + 1 )

With that in place, templated class EnumIterator becomes available for the enumeration type. The loop can be rewritten as follows:

for( auto element : alib::enums::EnumIterator<Pets>() )
{
// do something...
cout << UnderlyingIntegral(element) << endl;
}

3.2 Details On Iterable Enums

3.2.1 operators+/-(TEnum, int)

In the previous section we used

ALIB_ENUMS_MAKE_ITERABLE(Pets, Pets::Snake + 1 )

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.

3.2.2 Stopper Elements

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.

3.2.3 Starter Elements

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.

3.2.4 Helper-Type EnumIterator

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[].

3.3 Bitwise Enums And Iteration

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.

3.4 Performance Considerations

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

3.5 Using Class TBitSet with Iterable Enums

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:

enum class Pets
{
Cat,
Dog,
Bird,
Snake,
};

As soon as we include additional header:

#include "alib/enums/iterablebitset.hpp"

type definition EnumBitSet becomes available. This type simply fills out the right template parameters for target type TBitSet.
Those are:

  • The "interface type into the bitset", namely Pets. This allows use to use elements of this enumeration to pass as bit number specifiers.
  • The length of the enumeration by passing T_EnumIsIterable::End.
  • The start of numbering. In our case, this is 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:

pets.Set( Pets::Cat, Pets::Dog, Pets::Bird );

Because TBitSet provides efficient bidirectional iterator types that deliver just the bits that are set, a simple loop like this can be implemented:

cout << "Allowed pets: " << endl;
for(auto& it : pets )
cout << " " << static_cast<int>( it.Bit() ) << endl;

We flip the set and loop again:

pets.Flip();
cout << endl << "Forbidden pets: " << endl;
for(auto& it : pets )
cout << " " << static_cast<int>( it.Bit() ) << endl;

The output of this code is:

Allowed pets:
0
1
2
Forbidden pets:
3

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.

4. Enum Records

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:

  • A custom data record type is associated with an enum type.
  • One or more static data records may be defined for each element of such enum type.
  • The inheritance graph of the record types, may be used to constitute an inheritance relationship for enums. This allows phrasing questions like:
           "Is enum type X inherited from enum type Y?"
    This is answered at compile-time and for example allows a custom method to accept elements of an arbitrary enum types as an argument, as long as they are of a certain "base type".
  • In the presence of module ALib Strings in the ALib Distribution, enum elements can be easily serialized (to log files, configuration files, etc).
  • In the presence of module ALib BaseCamp in the ALib Distribution, data record definitions can be easily "externalized", for example with the aim of translating a software to different locales or for providing the ability to change default values of a software without recompilation and roll-out.

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.

Note
While the only dependency of this ALib Module is with ALib Singletons, and therefore the module compiles well in the absence of module ALib Strings in an ALib Distribution, this manual's samples use ALib string-types and thus to compile the samples that module has to be included.
By the same token it should be mentioned that the utility functions used for parsing (externalized) string-defined data records are implemented with ALib strings and hence are not available in the absence of module ALib Strings. It is therefore greatly recommended to include this module when using ALib Enum Records, independent of the fact if a user code intends to make use of ALib string types byond that or not.

4.1 Equipping An Enumeration With Data Records

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:

enum Fruits
{
Apple,
Orange,
Banana,
}
// print fruits
public void printFruit(Fruits fruit)
{
System.out.println( fruit );
}

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:

enum class Fruits
{
Apple,
Orange,
Banana,
};

It is only three simple steps to be done.

4.1.1 Step 1/3: Declaring The Record Type

First we need a suitable "record type" to store the element names:

struct ERFruits
{
String Name;
ERFruits( const String& name )
: Name(name)
{}
};

4.1.2 Step 2/3: Assigning The Record To The Enumeration

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:

#include "alib/enums/records.hpp"

The easiest way to do the specialization is by using macro ALIB_ENUMS_ASSIGN_RECORD as follows:

ALIB_ENUMS_ASSIGN_RECORD( Fruits, ERFruits )
Note
This is the only macro needed for the use of ALib Enum Records!
It has to be in the global namespace and usually is placed in same header file that defines the enumeration.

4.1.3 Step 3/3: Initializing The Data

We are almost done. The final step of preparation is to define the data records. This is to be done when bootstrapping a software.

Note
Enum records are "by contract" static data! The records once assigned cannot be changed. This is in alignment what the Java language sampled above offers: Also there, the name of the enumeration types are fixed and just equals the name given to the element in the source.
This restriction is not a technical one, but a design decision.
The benefits of this approach will be discussed in later chapters. For now it is enough to acknowledge:
Record definitions are to be done in the single-threaded, one-time executed, bootstrap section of 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:

#include "alib/enums/recordbootstrap.hpp"

This is a precaution to ensure 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:

alib::EnumRecords<Fruits>::Bootstrap( Fruits::Apple , A_CHAR("Apple" ) );
alib::EnumRecords<Fruits>::Bootstrap( Fruits::Orange, A_CHAR("Orange") );
alib::EnumRecords<Fruits>::Bootstrap( Fruits::Banana, A_CHAR("Banana") );

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:

{
{ Fruits::Apple , A_CHAR("Apple" ) },
{ Fruits::Orange, A_CHAR("Orange") },
{ Fruits::Banana, A_CHAR("Banana") },
} );

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.

4.2 Accessing Enum Records

To access the three enum records of type Fruits, two possibilities are offered.

4.2.1 Retrieving A Specific Record

In the Java sample above, method printFruit was provided as:

public void printFruit(Fruits fruit)
{
System.out.println( fruit );
}

Here is now the corresponding C++ code:

void printFruit(Fruits fruit)
{
cout << alib::enums::GetRecord(fruit).Name << endl;
}

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:

printFruit( Fruits::Apple );

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:

printFruit( Fruits(42) );

While no "named" enum element is given in the enumeration, still the language allows "constructing" 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:

void printFruit(Fruits fruit)
{
const ERFruits* record= alib::enums::TryRecord(fruit);
if( record != nullptr )
cout << record->Name << endl;
else
cout << "Fruits(" << UnderlyingIntegral(fruit) << ")" << endl;
}

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!

4.2.2 Iterating Enum Records

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<TEnum> 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:

for( auto& fruitRecord : EnumRecords<Fruits>() )
cout << fruitRecord.Name << endl;

and produces the following output:

Apple
Orange
Banana

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:

Fruits readFruit(const String& input)
{
for( auto it= EnumRecords<Fruits>::begin(); it != EnumRecords<Fruits>::end() ; ++it)
if( input.Equals<CHK, lang::Case::Ignore>( it->Name ) )
return it.Enum();
return Fruits(-1);
}

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:

Fruits someFruit= readFruit( A_CHAR("Banana") );
assert( someFruit == Fruits::Banana );

4.3 Detail Topics

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.

4.3.1 Serialization/Deserialization

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

{
{ Fruits::Apple , A_CHAR("Apple" ), 1 },
{ Fruits::Orange, A_CHAR("Orange"), 1 },
{ Fruits::Banana, A_CHAR("Banana"), 1 },
} );

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:

  1. Record type ERSerializable is assigned to an enumeration, and
  2. Header file
    #include "alib/enums/serialization.hpp"

is included, elements of that enum become appendable to instances of type AString:

AString buffer;
buffer << Fruits::Banana;
assert( buffer.Equals( A_CHAR("Banana") ) );

As with every type that is appendable to AString instances, with inclusion of header file:

#include "alib/compatibility/std_strings_iostream.hpp"

we can use std::ostream::operator<< likewise:

cout << Fruits::Orange;

For parsing enum elements back from strings, templated namespace function Parse is given:

Fruits parsedFruit;
Substring input = A_CHAR("Banana");
bool success= alib::enums::Parse( input, parsedFruit );
assert( success && parsedFruit == Fruits::Banana );

The built-in facilities to serialize and deserialize enumeration element are:

4.3.2 Enum Inheritance Relationships

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:

struct ERAnything { /* data members */ };
struct ERBase { /* data members */ };
struct ERDerived : ERBase { /* data members */ };
enum class Anything { element };
enum class Base { element };
enum class Derived { element };
ALIB_ENUMS_ASSIGN_RECORD( Anything , ERAnything )
ALIB_ENUMS_ASSIGN_RECORD( Base , ERBase )
ALIB_ENUMS_ASSIGN_RECORD( Derived , ERDerived )
// A function accepting enums of type Base or a "derived enum type"
template<typename TEnum> typename std::enable_if<
EnumRecords<TEnum>::template AreOfType<ERBase>() >::type
acceptBaseOrDerived(TEnum element)
{
const ERBase* record= alib::enums::TryRecord( element );
(void) record; // do something...
}

then, function acceptBaseOrDerived can be invoked with enums of type Base or Derived, but not with elements of type Anything:

acceptBaseOrDerived( Base::element );
acceptBaseOrDerived( Derived::element );
#if defined( I_WANT_COMPILATION_ERRORS )
acceptBaseOrDerived( Anything::element );
#endif

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.

4.3.3 Defining Multiple Records Per Element

It is allowed to define multiple enum records for a single element of an enumeration. If done, then

  • Methods GetRecord and TryRecord will retrieve the first record that was initialized during bootstrap.
  • The additional records can only be retrieved by iterating the records, as explained in previous section 4.2.2 Iterating Enum Records.

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:

{
{ lang::Bool::True , A_CHAR("False"), 1 },
{ lang::Bool::False, A_CHAR("True" ), 1 },
{ lang::Bool::True , A_CHAR("0" ), 1 },
{ lang::Bool::False, A_CHAR("1" ), 1 },
{ lang::Bool::True , A_CHAR("No" ), 1 },
{ lang::Bool::False, A_CHAR("Yes" ), 1 },
{ lang::Bool::True , A_CHAR("Off" ), 2 },
{ lang::Bool::False, A_CHAR("On" ), 2 },
{ lang::Bool::True , A_CHAR("-" ), 1 },
{ lang::Bool::False, A_CHAR("Ok" ), 2 }
} );

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:

{
{ lang::ContainerOp::Insert , A_CHAR("Insert" ), 1 }, // integral 0
{ lang::ContainerOp::Remove , A_CHAR("Remove" ), 1 }, // integral 1
{ lang::ContainerOp::GetCreate, A_CHAR("GetCreate"), 4 }, // integral 3 <-- Switched order
{ lang::ContainerOp::Get , A_CHAR("Get" ), 1 }, // integral 2 <--
{ lang::ContainerOp::Create , A_CHAR("Create" ), 1 }, // integral 4
} );

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,TAllocator>.

4.4 Resourced/Externalized Enum Records

As stated in the previous sections, ALib Enum Records are considered static data, which is "manifested" with three design decisions:

  1. Definition of records is allowed only during bootstrap,
  2. Records are constant, unchangeable data, and
  3. Access to records is not protected against race conditions in multithreaded software (and does not need to be, because 1. and 2.).

This is very well in alignment with common string resources that may either reside in the data segment of a process 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.

Note
This is achieved by code selection in this module ALib Enums, which provide additional interfaces for record definition if module ALib BaseCamp is included in an ALib Distribution.

4.4.1 Parsing Enum Records From Strings

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 parsing a single record or an array of records from a string. In alignment with the constraints of enum records, the string provided to this method has to be static itself, for example, a C++ string literal. This restriction implies that the records of single string type fields do not have to be edited. The strings in the record simply point to the corresponding substring of the given string after parsing!

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) is accessible through 100% static helper-type EnumRecordParser. The latter provides convenient methods to parse fields of string, integral, floating point, character and enum type.

Note
For details of how to implement method Parse for your custom type, consult the reference documentation of aforementioned entities.

With this knowledge, and the fact that the arguments defining the inner and outer delimiter characters default to ',', the definition of our sample enum Fruit's records changes now from:

{
{ Fruits::Apple , A_CHAR("Apple" ), 1 },
{ Fruits::Orange, A_CHAR("Orange"), 1 },
{ Fruits::Banana, A_CHAR("Banana"), 1 },
} );

to

A_CHAR( "0" "," "Apple" "," "1" ","
"1" "," "Orange" "," "1" ","
"2" "," "Banana" "," "1" ) );
Note
Although the parsing code has to be included by the compiler, the use of C++ string literals may lead to smaller footprint of an executable when reasonable numbers of records are defined. Should this be important to your software (e.g., embedded systems), a comparison might be done.

4.4.2 ALib Module BaseCamp And Enum Records

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.

4.4.3 Using TMP struct T_Resourced

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

Note
Within ALib, two record types exist that make use of this paradigm. In both cases, the using code determines if T_Resourced is specialized. In the case it is, some fields of the records are interpreted as names of further resources to load, and in case it is not, the fields value will be used instead.
The two types will be introduced in a later chapter.

4.4.4 Using Resources In ALib Modules

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.

4.5 A Design Pattern For Using Enum Records

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:

  • What is this good for? To be able to print out names of enum elements?
  • Why should I use this?
  • I could use a simple static instance of 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

  1. "Static data tables"
  2. Records are collected in a simple forward list to be iterable.
  3. Record are in addition hashed using a pair of a table name and a simple integral value as an index to the record.

From this angle of view, the only difference this concept brings is:

  1. A C++ enumeration type (!) is used for the name that a) addresses an iterable record list and b) is the first part of the hash key to retrieve single records. (Technically implemented by using run-time type information, aka keyword typeid).
  2. The numbering of the records of a table is done with the elements of that enumeration and used as the second part of the hash key.

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 at 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 a StringTree of "variables". These variables can either be declared in a "hard-coded" fashion or optionally be declared by meta-information struct Declaration.

This declaration struct extends built-in enum record type ERSerializable. While the variable name is taken from the base class, the record provides additional data needed to declare and define a variable with a reasonable default value in case no external configuration data is found

Then, the module provides constructors and declaration methods that accept elements of custom enum types, as long as those are equipped with enum records of type Declaration. (See the reference documentation of class Variable.)

Consequently, the user of the module declares a custom enum type that enumerates all variables that her software wants to store and retrieve externally. This custom enum type is associated with enum records of type Declaration. If the records are resourced, then variable names, description text and default values are nicely externalized and can be translated to different locales without recompiling the software.

This is all that needs to be done. The advantages are

  • All information about variables are defined in one place.
  • The code that stores and retrieves variables is 100% independent of the internals needed to declare variables. It just passes the enum element.
  • It is very convenient to declare and access variables. No misspelled variable strings may occur and a developer's IDE can propose the choice of elements while typing.
  • Special method Configuration::PreloadVariables goes even one step further: Just all elements, hence all variables of the enumeration types are preloaded with their default value with just one invocation.

Another prominent sample is found with class Exception. Furthermore, module ALib CLI makes really heavy use of this paradigm.

5. Class Enum Of Module Boxing

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 having custom methods with arguments that in turn accept arbitrary enumeration elements and that defer actions with these arguments for later.

With methods Enum::GetRecord and Enum::TryRecord, such postponement is also offered for the concept of ALib Enum Records.