This ALib Camp is the fundament for all other ALib Camps. An explanation to this statement is given in chapter 3. ALib Camps and Module BootCamp of the Programmer's Manual of ALib.
In addition to that, this module aggregates different sets of classes, which did not find another home and which absolutely made sense to place in this foundational ALib Camp.
As an exception from the general rules of organizing header files and namespaces, this ALib Module, respectively ALib Camp, adds files to the following subfolders of alib/lang, namely:
These files expose their types in corresponding inner namespaces of alib::lang, except (and that is a next exception of the rules), that the types defined with headers in alib/lang/message, expose their types directly to namespace alib::lang.
The following chapters address these four sets of header files.
As just explained, the following types defined in headers and compilation units of alib/lang/message are exposed in namespace alib::lang. This is because they are very fundamental and provide general ALib functionality.
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.
Objects of type Message relate to a source code location, which is reflected by field CI.
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 which 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 because otherwise using software's executable would include source information of ALib which may not be wanted.
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:
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.
The contents of the message is comprised by a list of boxed objects. For this, class Message inherits class BoxesMA, 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.
As documented with class TBoxes, its underlying std::vector
may use either standard dynamic memory or "monotonic allocation" introduced by module ALib Monomem. With class Message, monotonic allocation is chosen. A reference to the allocator has to be passed with construction. at that moment and only be initialized before adding information to the message. This supports to derive from class Message which hold the allocator instance of a member.
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.
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.
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 cannot be loaded, a type of "resource exception" may be thrown. Then, the caller of this method has different information, for example that it cannot 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:
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:
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 performs different actions depending on the exception entries.
Of course, a user of the library might break this rule and derive own exception types, but it is really not recommended. Think about it first! No ALib Module uses derived types and only this type needs to be "caught" when invoking ALib code that throws.
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 ensured 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 2.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 Enum is searched and returned with method 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 a catch handler uses method 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.
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 collecting many new entries and each entry, as being of type Message in turn may consist of a bigger collection of message arguments.
Nevertheless, the probably astonishing facts are that:
sizeof
class Exception equals sizeof(void*)
.This all is achieved by inheriting class Exception from class TSharedMonoVal. Please consult that types' reference documentation as well as chapter 7. Class TSharedMonoVal of the Programmer's Manual of module ALib Monomem.
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 2.1.5 Argument Life-Cycle).
The only operation that the destructor of class Exception performs is to delete the allocated memory buffer. 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 a smaller footprint than standard C++ library exceptions std::runtime_error
, it (usually) also performs only one single memory allocation to store all information collected while unwinding the stack.
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:
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 analyzed 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, placeholder {!X}
would suppress an argument completely. (Note: This is an extension to the original python formatter syntax.)
It is recommended to catch ALib Exceptions by reference:
catch( alib::Exception& e ) { ... }
While the footprint is only a single pointer, the call of the copy constructor would impose an unnecessary overhead similar to the overhead of copying an instance of C++ standard type std::shared_ptr
.
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 are to be caught by reference, rethrowing is recommended to be done as reference.
While the following code compiles and works:
catch( alib::Exception& e ) { throw e.Add( ... ); }
...this is the more efficient way to do it:
catch( alib::Exception& e ) { e.Add( ... ); throw; }
Being a self-containted type that implements a shared-pointer behavior, exceptions and their collected entries, can nicely be collected, for example for later analysis or repeated log output. This is done for example by just copy the exception as a value into a vector. As soon as the vector is cleared, all dynamic allocations that the exception instances performed (usually one allocation of 1 kilobyte per exception) are automatically freed.
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.
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.
ALib uses the term "resources" for string data that technically may be defined in a constant and static fashion, but that a software volunteers to make configurable. Typical samples of data that a software exposes for external management are "themes" ( color and font schemes), or so called "externalized strings", which mostly may be used to translate a software into a different "locale" and human language.
While the conceptual definition of resources is very similar to the concept of configuration data, it is good practice to thoroughly decide for each specific piece of data that is deemed to be made configurable, whether it is resource or configuration data. While the mentioned samples of language translation strings and color schemes are typically resources, a server name that a software attaches to is typically configuration data. End users commonly know and understand the difference between the two intuitively and appreciate if the corresponding data sources are separated, especially as this way, resource data does not "clutter" the definitions of configuration sources.
The best rule of thumb to differentiate resource from configuration data is to check if it is possible to distribute a software without turning the data in question into either of the two externalization concepts, thus "hard coding" the data. If so, the data more likely is resource data. In the sample of the "server address" above, this is not possible if such server address targets a machine that is under the control of an end user. However, if it is a machine provided by the distributor of the software which is the same with any installation, then the concept of resourced data rather fits.
As documented in chapter 2. ALib Modules of the ALib Programmer's Manual, the various ALib Modules can be separated into those who are ALib Camps and those which are not. In this module dependency graph it could be noted that all ALib Camps (dark blue) are dependent on module ALib BaseCamp, discussed in this manual.
This relation is explained as follows:
Therefore, all ALib Camps not only depend on module ALib BaseCamp, but also dispose about a singleton type derived from class Camp.
ALib resources implemented with this namespace, conceptually impose a set of rules which may be named a "contract", "invariants" or just "determinations". These rules are given in the following subchapters.
Key values to resources are defined as a two level hierarchy of a category and a name. This determination complies with how configuration data is addressed with module ALib Configuration. This is also in alignment with common 3rd-party resource management systems established in the industry.
Independent of the compilation options of ALib in respect to choosing the default character width, the string-type of the category and name is fixed to NString, hence using the narrow nchar type. This is not in alignment with configuration data as defined in ALib Configuration. The rationale for this is that while configuration data categories and names may be translated themselves to localized versions (preferably by defining those strings as resources!), the category and name strings of the resources are deemed to be hard-coded in the source code that accesses resources. As such, they are not shared with end-users, are never localized, and should be using the plain 7-bit printable ASCII character set.
All resource data is of String type (of compilation-dependent character width).
This restriction imposes that any other data type (for example color codes) has to be de-serialized (decoded) when resourced.
Although resource data technically is non static data, conceptually with this implementation it is.
This determination has the following impacts:
Accessing resources using abstract interface method ResourcePool::Get is a thread-safe operation.
In contrast to this, an invocation to any of the methods that define resources, namely ResourcePool::Bootstrap and ResourcePool::BootstrapBulk is not a thread-safe operation. This is true in respect to each other as well - and most important - also in respect to parallel resource access with method Get.
This determination has the following impacts:
The central type of the module, class ResourcePool was already mentioned several times. It constitutes a pure abstract interface. Due to the determinations of the concept given in previous chapter 3. Data Contract / Conceptual Invariants, its interface is very simple especially in respect to accessing resources, which is exclusively done with method ResourcePool::Get.
A user of ALib should have no bigger effort to implement this interface and adopt her own or any 3rd-party "backend" that performs the resource management and the externalization of strings.
Apart from that, two implementations of the interface are provided with ALib. Those are quickly introduced in the following sections.
As explained above, an implementation of interface ResourcePool has to be constructed during bootstrap of ALib and distributed among the modules.
In case the bootstrap process is not customized, an instance of class LocalResourcePool is created and shared.
This class does not allow any externalization of resources and simply stores the given pointers to the static data in a HashTable, using monotonic allocation.
A second built-in implementation of class ResourcePool which can be created and shared among the modules of ALib by customizing the bootstrap process of the library, is given with class ConfigResourcePool.
The type externalizes resources by inheriting from class Configuration. With that, flexible ways of externalizing the data are given. The class is enabled to fetch all "hard-coded" resources fed by camps with methods Bootstrap and BootstrapBulk.
If after creation of an instance of the type, this instance is not changed and just used, then it behaves in an identical way as type LocalResourcePool (with only weaker performance and increased memory overhead).
The huge advantage of this type is, that any custom externalization is achieved along the very same lines as done with configuration data. Thus, a user of the library that attached such data, will have no problem in also load resources from external custom sources.
The type is suitable in any situation where no other ("more professional") 3rd-party "backend" for resource management is available.
Here are some tips for the usage:
Sometimes it is required to define resource information, namely
for use by other components. TMP struct T_Resourced may be specialized to do such definition for C++ types. A specialization of the struct can be easily implemented using macro ALIB_RESOURCED.
A sample for the use of this struct is given with module ALib Configuration: To load and store configuration data, this module exposes a type of ALib Enum Records and accepts custom enumeration types in various interface methods, if they just have this specific record type associated.
Now, if an element of a custom enumeration type that disposes about a specialization of T_Resourced is passed to such interface method, internally this information is used to load further details of the variable from the resource pool.
As soon as struct T_Resourced is specialized for a type, helper-struct static struct ResourcedType becomes available.
The type has two overloaded methods Get: The first is parameterless and simply receives the resource string associated to a type with the specialization of T_Resourced. The second takes a replacement for the resource name. This may be used to retrieve resource strings which are likewise associated to the type.
Furthermore, the struct provides methods TypeNamePrefix and TypeNamePostfix which are meant to provide a standardized way to define a type's name using resources. The methods are for example used with specializations T_Append<TEnum,TChar,TAllocator> and T_Append<TEnumBitwise,TChar,TAllocator> which write enum element names into instances of type AString.
A next helper-struct is given with ResourceInfo which first of all is a simple struct that stores resourcing information (the resource pool and category and name strings) for later use.
While this struct is usable without a specialization of T_Resourced, in most use cases it is, because it allows converting the compile-time information given by T_Resourced into run-time information.
Besides just externalizing strings, many use cases require to access externalized data sets or even whole tables of this.
ALib module ALib Enums provides a solution for this with its concept ALib Enum Records. There a table of data is addressed using the C++ type information of enum types. Single records of a table may (or may not) be addressed by elements of the corresponding enumeration. The module provides convenient facilities the fields of the records and whole tables from resourced strings.
Before you go ahead and implement your "own" solution for externalized static data, it might be worthwhile to check out if ALib Enum Records meet your requirements.
When resources are externalized, for example for translation to different human languages, the list of resources have to be imported to the external "backend". To do so all resources have to be queried from the library.
Here is a sample program that performs this task:
The output of this sample can directly be loaded by class IniFile, hence with a plug-in attached to an instance of built-in resource pool implementation ConfigResourcePool. The sample might be adopted accordingly to write a format suitable to be imported to the backend of choice.
With every update of the library, changes of the resource strings have to be determined. This might be done for example with a custom unit test that compares the default entries with the ones currently stored in an external backend. New resources might be added, others might disappear, and worst: existing ones might change their content format. In the latter case, an externalized resource might be errorneous and lead to undefined behavior.
Starting with library version 1903, to support the detection of changes, the version history is found in
ALIB_BASE_DIR/docs/pages/resource-exports/
The following exports are available:
With the provision of compiler symbol ALIB_DEBUG_RESOURCES, static field LocalResourcePool::DbgResourceLoadObserver becomes available. If set to &std::cout
before bootstrapping ALib, the resource load process can be observed on the console, because methods LocalResourcePool::BootstrapBulk and LocalResourcePool::BootstrapAddOrReplace will write information on bulk and singular resource data definitions. This tremendously helps to find errors in resource strings, which often are simply missing commas and similar.
Next, virtual method ResourcePool::DbgGetCategories and ResourcePool::DbgGetList become available. The latter returns a list of all resources defined, including a usage counter. The method is only implemented by class LocalResourcePool. Alternative type ConfigResourcePool does not implement it.
Furthermore, convenience method ResourcePool::DbgDump becomes available which simply writes the list of symbols into an AString, sorted by category and in an alphabetical order.
The usage counter included in the list may be used to identify two groups of resource strings:
While entries of the first type may have become obsolete and are candidates for removal, those of the second type, a software might consider to "cache" the symbol in a variable instead of repeatedly retrieving it from the resource pool.
Remember: While trivial implementation class LocalResourcePool is very fast and resource access is not really noticeable, other implementations might not be.
The following code snippets taken from the ALib unit tests, demonstrate how to quickly leverage these debug features, depending on compiler symbol ALIB_DEBUG_RESOURCES. The snippets might be copied to own projects and remain there forever.
Bootstrapping may look as follows:
Before termination of a software (hence, before invoking Shutdown), the following code may be placed:
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.
This ALib Module provides string formatting facilities by implementing an approach that is common to many programming languages and libraries. This approach offers an interface that includes the use of a "format string" containing placeholders. Besides this format string, a list of data values can be given, used to fill the placeholders.
Probably one of best known samples of such an interface is the printf
method of the C Language. A variation of this interface is found in almost any high-level, general purpose programing language.
Of course, this module leverages module ALib Strings for all general string functions needed. Similar important is the use of module ALib Boxing, which brings type-safe variadic argument lists and allows with its feature of having "virtual functions" on boxed arguments, to have custom formatting syntax for placeholders of custom argument type.
While it is possible to implement a formatter providing a custom placeholder syntax, two very prominent ones are built-in with formatters:
printf
format string style.Another good news is that in its very basics, Python Style is similar to .Net formatting. This way, there is some "familiar basic syntax" available for everybody that has used formatting in one of the languages C, C++, C#, Java, or Python and in languages that have also mimicked one of these styles!
By leveraging module ALib Boxing, which implies the use of variadic template arguments, the invocation of the final format method is as simple as it is possible. The following samples a simple format action with each of the two built-in formatters:
This produces the following result:
Values of or pointers to any type that is "boxable" may be passed as an argument to method Formatter::Format. The specific implementation of the formatter will match the "placeholder type" with the given argument type and format the argument according to the placeholder attributes.
In the sample above, two different formatters are created and each is used "properly", namely with its according syntax.
To increase flexibility, the formatters of this ALib Module provide two features:
With that information, the following code can be written:
new
) then stored in field Next of the first formatter. This field is of type SPFormatter, which is an alias for std::shared_ptr<Formatter>
, hence a C++ standard "smart pointer" that deletes it's contained object automatically with the deletion of the last referrer. Only in later chapters it will be explained why it is the preferred method to manage ALib formatter instances in shared pointers.The short sample code correctly produces the following output:
However, the placeholder syntax must not be mixed within one format string. Let's try:
The output is:
---Java---{}---Python
This is obviously not what we wanted, but then it also did not produce an exception and it even included the second argument, "Python" in the output. While exceptions are discussed in a later chapter only, the reason that no exception is thrown here is simply explained: The first formatter in the chain, which we defined as type FormatterJavaStyle, identified the format string by reading "%s". It then "consumes" this string along with as many subsequent arguments as placeholders are found in the format string. This number is just one, as the placeholder "{}" is not recognized by this formatter.
The intermediate result consequently is "---Java---{}---", while the argument "Python" remains unprocessed. The next section explains what happens with this remaining argument.
We just continue with the sample of the previous section: The unprocessed argument "Python" is not dropped, as it would have been with most implementations of a similar format functions in other libraries and programming languages. Instead, with ALib BaseCamp, the formatting process starts all over again using the remaining argument as the format string.
Now, as it is not a format string (it does not contain placeholders in any known syntax) it is just appended to the target string "as is".
In fact, for this last operation, none of the two formatters became active. The trick here is that the abstract base class, Formatter already implements method Format. This implementation loops over all arguments. It checks if the current first argument is a string recognized as a format string by any of the chained formatters. If it is not, this argument is just appended to the target string and the loop continues with the next argument in the list.
If a format string is identified, control is passed to the corresponding formatter that consumes as many further arguments as placeholders are found in that format string, and then passes control back to the main loop.
Consequently, this approach allows invoking Format without even a format string:
123
which probably does not make any sense, because the same result could have been achieved much more efficiently by stating:
Even the following sample, still might not make too much sense to a reader:
because the usual way to come to the same result, was to have only one format string with two arguments, instead of two format strings with one argument each:
So, why is this loop implemented with its auto-detection and the option of having more than one format string? Some sound rationale for the loop is given in the next section.
Method Format collects the variadic template arguments to an internally allocated container of type TBoxes. This container is then passed to the internal format loop.
Alternatively, user code may perform the collection of format arguments in a container object "manually". In this case, the formatter is invoked with one of the two overloaded methods Formatter::FormatArgs. For this, either an external container can be used, or the internal container may be received with the method Formatter::GetArgContainer. The latter case saves allocation effort as the returned vector is never freed but reused instead. However, a user must be aware of racing conditions in multithreaded software, especially when working with the global instance Formatter::Default.
External containers might of-course also be of derived type, for example type Message may be passed.
With this knowledge it becomes obvious that the collection of formatting arguments can be "decoupled" from the invocation of the formatter. Note that the argument list may include zero, one, or even multiple format strings, which each are followed by corresponding placeholder values:
A reader might think for herself if and when this might become useful. It should be noted that the interface of logging module ALox builds on the same mechanism. The arguments there are called "logables" and might be format strings or anything else. Therefore, also with ALox the collection of log entry data can be decoupled from the final creation of the log entry. This is especially useful for complex log-entries whose arguments are collected during the execution of an algorithm and for example are only logged in case of an exception or other unexpected conditions.
In the previous samples, a local instance of a formatter (or two) has been created. For general purpose use, this module provides a global pair of (concatenated) formatters which are accessible static member Formatter::Default.
The formatter is embedded in "automatic pointer type" SPFormatter, which builds on SharedPtr. During bootstrapping of the library, a formatter of type FormatterPythonStyle is created with a concatenated object of FormatterJavaStyle.
One obvious rationale for the provision of these default formatters is to save resources by reusing the formatter instances in different parts of an application. However, probably in most cases more important is the fact that this way, the same default configuration is used with formatting operations. For example, if the decimal point character of floating point numbers should be defaulted to be different from US/English standard '
.', then such a setting could be performed with the bootstrap of the library once and for all usages across a process.
With multithreaded software, the default-formatter is to be locked using mutex Formatter::DefaultLock. This is asserted in debug-compilations, as described in detail in chapter 1.4.3 Asserting Critical Section Locks of the Programmer's Manual of module ALib Threads.
Formatter implementations may or may not provide default settings that, for example, influence a format operation that uses minimal placeholders that omit optional formatting flags. The built-in formatters do have such default settings.
If a software unit wishes to change some settings, the advised approach is as follows:
With this procedure, any changes that an application applied to the default formatters (e.g., during bootstrap) will remain valid in the cloned formatters in addition to the "local" changes, while the default formatters remain untouched.
For example, built-in formatters provide in fields DefaultNumberFormat and AlternativeNumberFormat to reflect some default behavior of their formatting syntax.
The attributes of these members might be modified to change those defaults. While this leads to a deviation of the formatting standard, it may be used instead of providing corresponding syntactic information within the placeholder field of every format string. Some modifications may not even be possible with the given format specification syntax.
The simple samples shown so far used correct format strings. In case of errorneous format strings, the built-in formatters will throw an ALib Exception defined with enumeration alib::lang::format::FMTExceptions.
While in the case of "hard-coded" format strings, such exceptions are not needed to be caught, their evaluation (with debug-builds) might be very helpful for identifying what is wrong. Of course, when format strings are not hard-coded but instead can be provided by the users of a software (for example in configuration files or command line parameters), a try/catch
block around formatting invocations is a mandatory thing, also in release compilations.
The following sample shows how an exception can be caught and its description may be written to the standard output:
The output of running this code is:
In most cases, a detailed text message, including a copy of the format string and a "caret" symbol '^'
that hints to the parsing error in the string is given with the exception's description.
Escape characters, like for example "\t"
, "\n"
or "\\"
might be given with either one or two backslashes. The formatters will convert them to the corresponding ASCII code, if the backslash itself is escaped.
Class FormatterPythonStyle recognizes double curly braces "{{"
and "}}"
and converts them to a single brace. Similar to this, class FormatterJavaStyle recognizes "%%"
and converts it to a single percentage symbol.
As we have seen, the use of module ALib Boxing allows the formatters of this module to accept any third-party data type as formatting arguments. The formatters of course are enabled to "convert" all C++ fundamental types to strings. But how about custom types?
The solution for custom conversion is given with the support of "box-functions", which implement a sort of "virtual function call" on boxed types.
There are two box-functions that the built-in formatters are using.
By default, the very simple box-function that is used by the built-in formatters for converting arbitrary types to string values, is FAppend. This function is one of the built-in functions of module ALib Boxing and this way is not specific to this module ALib BaseCamp.
Usually this function's implementation just unboxes the corresponding type and appends the object to the target string.
Let as look at an example. The following struct stores a temperature in Kelvin:
If an object of this class is used with a formatter without any further preparation, the default implementation of function FAppend is invoked, which writes the memory address of the given object. In debug-compilations, this implementation in addition writes the boxed type's name (platform-dependent and implemented with class DbgTypeDemangler). This is shown in the following code and output snippet:
The temperature is Kelvin(Size: 8 bytes)
The first step to implement function FAppend for sample type Kelvin is to specialize functor T_Append for the type:
With that in place, it is possible to apply an object of this type to an AString:
14.5 ℃
Now, we can easily implement box-function FAppend, because for types that are "appendable" already, this is done with just a simple macro that has to be placed in the bootstrap section of a software:
With that in place, it is possible to append a boxed object of this type to an AString:
14.5 ℃
Because the formatters use the same technique with the boxed arguments they receive, our sample class can now already be used with formatters:
The temperature is 14.5 ℃
To summarize this section, some bullet points should be given:
The previous section demonstrated how a custom type can be made "compatible" to ALib formatters found in this module ALib BaseCamp.
The given approach using box-function FAppend is quite limited in that respect, that within a format string no attributes might be given that determine how to format a custom type. With the sampled temperature type "Kelvin", the output format was in celsius with one decimal digits. If we wanted to allow Fahrenheit as an alternative output, we need to implement boxing function FFormat, which was specifically created for this purpose and consequently is defined in this module.
The function has three parameters: besides the box that it is invoked on and the target string to write to, it receives a string that provides type-specific information about how the contents are to be written. This format specification is fully type- and implementation-specific and has to be documented with the specific function's documentation.
We want to implement a format string that starts with character 'C'
, 'F'
or 'K'
to specify celsius, fahrenheit or kelvin and a following integral number that specifies the fractional digits of the output.
To do this, the following function declaration needs to go to a header file:
Then, the implementation of the function has to be placed in a compilation unit. This might look like this:
Within the bootstrap section of the process, the function has to be registered with ALib Boxing:
With that in place, we can use the custom format specification with our custom type
The following output is produced.
As a second sample we want to look at the internal implementation of formatting date and time values. ALib class CalendarDateTime provides (native) method Format to write time and date values in a human-readable and customizable way. This method also requires a format specification. Now, this helper-class is used to implement FFormat for boxed arguments of type DateTime, which is given with class FFormat_DateTime. Due to the existence of the helper-class, the implementation of the function is therefore rather simple:
To implement a custom formatter that uses a custom format string syntax, no detailed manual or step-by-step sample is given here. Instead, just some hints as bullet points should be enough:
Besides the formatter classes that have been discussed in this Programmer's Manual, module ALib BaseCamp provides some other types which make use of the formatters.
As of today, these types are
Please consult the class's extensive reference documentation for more information about the features and use of these types.
This inner namespace of ALib BaseCamp provides types that interface into the operating system of the host computer. The types found here are just a very few and this namespace is not meant as being anything of huge value for third party applications. Rather, the types have been implemented when other parts of ALib needed corresponding functionality.
That being said, we refer the reader to the reference documentation.