ALib C++ Library
Library Version: 2312 R0
Documentation generated by doxygen
ALib Module Results - Programmer's Manual

1. Class Message

When measured in source code, the size of class Message is very short. Nevertheless, as the type leverages almost all features of module ALib Boxing, it is a quite powerful tool to have.

The type acts as an information object of the central throwable type Exception, discussed in the next chapter. While class Exception stores a whole list of messages, only one single object is used with class Report, which is discussed in the final chapter of this small manual.

1.1 Source Location

Objects of type Message relate to a source code location, which is reflected by fields File, Line and Function.

The inclusion of the Function, which is of-course a redundant piece of information, already indicates that this source information has a general notion of "debug information" and at least is supposed to be presented to experts only.
Interface methods that create a Message object and for this expect the caller's source information, are usually invoked using a preprocessor macro to provide the three arguments. There are two different macros defined for this purpose, ALIB_CALLER and ALIB_CALLER_NULLED. The first macro provides the corresponding built-in preprocessor symbols for the source file, line number and currently compiled method/function name in any compilation configuration. The second macro is equal to the first only in debug-compilations. In release (or other non-debug) builds, macro ALIB_CALLER_NULLED provides nulled values for the arguments.

It depends on the using software which macro should be used. It must be noted, that with the use of macro ALIB_CALLER, even in release compilations, information about the name and location of C++ source code files and method names is included in the data segment of the executable or library built. Hence this is rather a "political" or "commercial" question to answer. Internally, ALib exclusively uses the "pruning" version ALIB_CALLER_NULLED. This is due to the fact that otherwise a using software's executable would include source information of ALib which may not be wanted.

Note
There is a third macro existing with ALIB_CALLER_PRUNED, which is empty with non-debug compilations. This macro is to be used with code that completely omits source information parameters in release compilations, which is not true for class Message and therefore not applicable with the creation of Message objects.

1.2 Message Types

To define the type of a message, class Enum provided by module ALib Boxing is used, by letting field Message::Type be of type Enum.

This leads to having a two-level hierarchy for defining message types. What is meant by this, should be explained by looking at other options:

  • If field Type was of integral type, it would be no hierarchy, but just a "flat" numbering system. With that it would be very problematic to find a numbering scheme that allows software modules that do not "know of each other" to define message types without ambiguity.
  • If the field was of a virtual base type, it would be a hierarchy of arbitrary depth: A message type class might be derived from the virtual base class, which in turn has derived types. Now, when investigating into a message's type, a user could try to dynamically cast the base type to a certain derived message type and on success would know that the message is of that type or a derived type.

By using class Enum, there are exactly two hierarchy levels: the first is accessed with method Enum::IsType. With that it can be determined if the "boxed" enumeration element is of a certain scoped enum type. To check both levels at once, Enum::operator== can be used.

Note
  • While this is not recommended in common use cases, method Enum::Integral allows to to test only the second level of the hierarchy, which is the underlying enum element's integral value.
  • It is also not recommended to use non-scoped (anonymous) enumeration types. While two elements of two anonymous enum types are well distinguished with Enum::operator==, the use of Enum::IsType is rather tricky and not well readable: the declaration type of a sample element has to be given as the template parameter to the method.

1.3 Message Arguments

The contents of the message is comprised by a list of boxed objects. For this, class Message inherits class Boxes, which itself is a container (vector) of objects of type Box. The interpretation of the data given in a message is defined by an application. As described in a later section of this manual, ALib class Exception, which includes a whole list of Message objects, proposes a certain schematic for the attached data. But also in this case, the proposal is just a recommendation, not a mandatory requirement.

1.4 Memory Allocation

As documented with class Boxes, its underlying std::vector may use either standard dynamic memory or "monotonic allocation" introduced by module ALib Memory. Such choice is also exposed with class Message by offering two constructors with the only difference that one takes a MonoAllocator in addition to the arguments that the other constructor expects.

This way, instances of type Message may be used as "stand alone" objects (with usual dynamic memory allocation) or might be embedded in other objects that may - if using monotonic allocation - not only allocate the message itself in memory chunks, but also have the message's arguments using that same allocator.

Note
Later in this manual, class Exception is discussed, which carries a whole list of messages that share one monotonic allocator.

1.5 Argument Life-Cycle

Restricted life-cycles of a message's arguments is a potential pitfall. Because class Message is a vector of boxes (by inheritance), for non-trivial arguments, a pointer to the corresponding original object instance is stored.

Note
For details on how a value of a certain type is "boxed" when added as an argument to class Message, wider parts of the programming manual of module ALib Boxing have to be understood. As a rule of thumb: objects that have a bigger size than 2 x 64 bit (respectively 2 x 32 bit on a 32-bit system) will not be copied, but a pointer to the original is stored.

Especially in the case of C++ exception handling (what the type is used for), this means that boxed objects must not be stack-allocated, because the stack is "unwinded" when the exception is thrown.
Therefore, (complex) stack-allocated objects and all other objects that might not survive the life-cycle of the message, need to be copied if they should be used as a message argument.

One solution for this and similar cases is already offered by module ALib Boxing with built-in box-function FClone. This function expects an object of type MonoAllocator that is used to allocate memory for cloning the contents of boxed data.

See also
For more information, see the reference documentation of box-function FClone and chapter 12.6 Life-Cycle Considerations of the Programmer's Manual of module ALib Boxing.

In the case that the Message object itself uses monotonic allocation for the internal vector (see previous section), parameterless method Message::CloneArguments may be used, which is a simple inline that passes the internal allocator to inherited method Boxes::CloneAll.

2. Exceptions

2.1 Exception Handling Paradigms

Class Exception is used within ALib as the one and only type thrown with C++ keyword throw.

Different paradigms or "design pattern" exists for the C++ language, proposing how to implement exception handling. There is a lot of discussion about this going on in the community and different paradigms have different advantages. For example, one alternative is to throw one exception object and while the stack is unwinded, this original exception gets replaced (by intermediate exception handlers) with a more meaningful one. This replacement object might be derived from the original one or belong to a different exception hierarchy. For example, if a resource string can not be loaded, a type of "resource exception" may be thrown. Then, the caller of this method has different information, for example that it can not access a server (because the server name was meant to be read from the resource) and now replaces the resource-related throwable with a server-access-related one.

ALib adheres to a different pattern and class Exception supports, respectively implements this. An exception once thrown is never replaced. Instead, while unwinding the stack new "entries" are added to the exception. The entries are of type Message, introduced in the first chapter of this manual.

Now, a new entry can have two effects:

  • Either the new message is "overwriting" the meaning of an exception and this way changing the overall exception code,
  • or the new message extends the existing meaning of an exception by adding detail information to the exception. Such detail information is generally information that only the "caller" of a method that caused the exception can add from it's context.

While this sounds complicated, it is not: the whole class is not much more than a list of objects of type Message and the exception information passed with the constructor of the class, simply becomes the first entry in the list. Each new message, regardless if the message changes the meaning of the exception or extends it with detail information, is simply appended to the list of entries. The exception just keeps track of the most recent entry that changed the meaning, while the overall order of message entries is always kept intact.

With this approach, all information "along the call stack" is preserved in the order of the information occurrence.

This allows:

  • Exception handling code to inspect "older" entries and perform different actions depending on the original cause.
  • Exception output that gives more meaningful information about what happened exactly, e.g. during software development or when logging exceptions "from the field" for later analysis.

Just to be clear: class Exception is not designed to be a base class for derived exception types. The recommended use of the class is that the type itself is the exclusive throwable. Consequently, when catching ALib exceptions, only one catch block is needed (possible) and within this catch block code might to perform different actions depending on the exception entries.

Of-course, user programmer might break this rule and derive own exception types, but while this is not recommended, no ALib Module uses derived types and only this type needs to be "caught" when invoking ALib code that throws.

2.2 Exception Types

With the paradigm/design pattern implemented (as discussed in the previous introductory section), usually an important problem is raised: What can be used as an "error type" and how can it be assured that error types defined in independent code units of a software are not ambiguous? Remember that the more common approach to implement exceptions, which is also taken in the C++ standard library that defines class std::exception to be a general base class for exceptions, such ambiguities do not occur: There, the C++ type system is used to distinguish exceptions. With the ALib implementation however, the entries need to use a "type-code" and these codes must not overlap.

This problem is solved by using type Message as entries of class Exception. As discussed in previous chapter 1.2 Message Types, this way a two-level hierarchy of exception types is established, where the first level uses run-time type information and hence no clash of exception types may occur, even between different code units.

There is one question left: Which of the messages in the collected list now does define the resulting "exception type"? Is it the first message added? Or the last one? In the paradigm ALib uses it might be any of them, or even one in-between.

The exception type is defined as: "The type of an exception is defined by the most recently added message, who's enum element's integral value is positive."

This definition is implemented with interface method Exception::Type. The method loops from the oldest to the newest message entry and searches the last one with a positive integral value in field Message::Type.

With this definition, enumeration element values of exception message entries that are considered "additional information" are to be declared with a negative integral initializer, while those that constitute an own exception type, are to be declared with a positive value.

Consequently, if the a catch handler uses method Exception::Type it does not need to switch over just all possible entry types (aka enumeration elements), but just over those that are positive and that therefore bring a different meaning to an exception.

2.3 Self-Contained Allocation

When an exception is thrown, something "bad" happened. In that situation a software should not perform too many complex things, for example dynamic memory allocations.

Simple exception types are therefore very lightweight, as for example class std::runtime_error which contains a simple, dynamically allocated string message.

The exception type introduced here seems in contrast rather heavy! It allows to collect many new entries and each entry, as being of type Message in turn may consist of a bigger collection of message arguments.

To minimize the number of heap allocations, class Exception implements a quite tricky approach. For all allocations (entries and their arguments of type Box), an object of type MonoAllocator is used. We had learned in chapter 1.5 Argument Life-Cycle that such allocator is used by the messages and thus this part of efficient allocations is solved already.

Secondly, class MonoAllocator allows to locate itself in a first block that it allocates. Thirdly, while this self-contained monotonic allocator is a member of class exception, the true implementation of the class is then still allocated using the monotonic allocator! The astonishing result is, that the footprint (sizeof) class Exception is just one simple pointer! Hence, when the exception is passed (by the C++ compiler) to a next "stack frame" while unwinding the stack, only this simple pointer is copied.

In addition, unless the attachment of many messages and data exceeds the initial allocated chunk of memory, only one single dynamic memory allocation is performed for storing the exception itself along with it's data.

Each time an entry is added with method Exception::Add, a new message object is allocated in the monotonic allocator and the values of the given boxes are cloned (see previous chapter 1.5 Argument Life-Cycle).

The only operation that the destructor of class Exception performs is to delete the allocated memory chunks. This is because none of the contained objects needs further destruction calls. Neither the boxed objects, nor the vector of such, nor the monotonically allocated list of message entries.

As a result, the rather complex type Exception becomes extremely lightweight. While it has the same minimum footprint as exceptions found in the standard C++ library, it usually also performs only one single memory allocation to store all information collected while unwinding the stack.

2.4 Exception Entries And Arguments

We learned so far that class Exception uses a list of entries of type Message, discussed in the first chapter of this manual.

Besides the source information and the message types, there is a third aspect that class Exception leverages from its entry type Message: the list of arbitrary arguments of arbitrary length.

But what are the message arguments used for in exceptions and what arguments should be added to exception entries? The answer is that this is completely use-case specific. For example, the provided arguments might help to implement an exception handler that mitigates the situation by being enabled to analyse in detail what happened.
In parallel, usually a human readable textual description of the exception is desired.

To have just both - a human readable textual description and computational values representing the state that caused the exception - it is recommended to adhere to the following simple scheme for the arbitrary argument list: The first argument should be a string which comprises a format string containing placeholders and following a syntax usable with ALib Formatters (or custom formatter syntax implementations). Then this string is followed by the "computational" arguments that comprise valuable information about the state of the software when an exception was thrown.

Of-course, all exceptions thrown by ALib itself adheres to this scheme. To support the generation of human readable exception descriptions, a ready to use formatting feature is given with overloaded methods Exception::Format.
In addition, when using the debug- and release-logging facilities provided with module ALox, exceptions can be comfortably logged with static method LogTools::Exception. In the latter case, during development, a very helpful and comfortable feature is that each exception entry collected during unwinding the call stack is "clickable" in an IDE's output window, so that the source code that created an entry can be displayed and analysed right away.

Note, that both formatting options (the built in method and ALox) are respecting the agreement of message entry types having either positive or negative values. As explained above, this agreement is constituted by method Exception::Type that searches the most recent enty with a positive enum element value. "Informational entries" are marked as such in the output.

Considering the message arguments to be doubly used, once while formatting an exception description and once while analysing the cause of an exception, a small conflict may arise: If a catch handler may need some information that should not be used with the description. In this case, a corresponding "suppression placeholder" may be added to the format string. For example, using the python-style formatter (FormatterPythonStyle), placeholder {!X} would suppress an argument completely. (Note: This is an extension to the original python formatter syntax.)

2.5 Catching Exceptions, Adding Information, Rethrowing

While objects of type Exception is extremely lightweight, consisting only of a single pointer (remember that all information is stored in an object of type MonoAllocator, including not only the monotonic allocator itself, but also the whole exception type itself!), they are not copy-constructible.

Therefore, they must be caught only by reference:

     catch( aworx::Exception& e )
     {
        ...
     }

In the case an exception should be rethrown, method Exception::Add may be used to add one or more new entries to the exception instance. Method Add has the very same signature as the constructor of the exception.

For the same reasons that exceptions have to be caught by reference, rethrowing has to. The following code does not compile:

   catch( aworx::Exception& e )
   {
      throw e.Add( ... );
   }

Here is the right way to do it:

   catch( aworx::Exception& e )
   {
      e.Add( ... );
      throw;
   }

2.6 Resourced Exceptions

To enable the external maintenance of the description strings of exception entries (as explained in previous chapters, it is proposed and recommended to add a descriptive, human readable string as a first argument to each exception entry), this module uses the concept of ALib Enum Records. This concept in turn allows the definition of records be performed using externalized string resources.

When custom enum types are associated with ALib Enum Records of type ERException, elements of this type become accepted by method Exception::Add which internally adds the resourced description string.

Please consult the reference documentation provided with the links in this section for further information.

3. Reports

What today is called "ALib Reports" is a rather simple concept of raising C++ assertions, similar warnings and general messages, currently all of them in debug-compilations only.

The concept is implemented with types:

Furthermore, module ALox introduces plug-in

The concept is not very advanced and might undergo changes in the future. Consequently, we do not recommend the use of Reports in custom code outside of ALib and therefore, apart from the reference documentation of the classes listed above, no Programmer's Manual exists.