Core Module

The core module is the basis for all other modules of the framework. It has no dependency on any system specific libraries except the standard C++ library.

This chapter covers program options, application settings and events, fixed-size integers, dates and times, atomic operations, coroutines, the core module's custom allocators, its unicode text processing, its signal and delegate callback mechanism, its type traits and conversion utilities, and its serialization framework.

Basic Types

Basic application support is provided by the Pt::Arg class and the Pt::Settings class. The first one is a convenient way to parse and process program options and the latter one allows to load and store application settings in files or other places.

Pt::Event is the base class for type-safe event objects dispatched by an event loop.

Pt::Any holds a value of any default- and copy-constructible type, and Pt::Void is a marker type used where void cannot be used as a template argument. Pt::NonCopyable is a mixin base class that disables copy construction and assignment, and Pt::Singleton implements the singleton pattern for a type.

Events

Specific event objects, subclass from Event and implement the onClone(), onDestroy() and onTypeInfo() methods. The first two are used to copy event objects for example in an EventLoop and the latter one is used to dispatch events by type.

Command Line Arguments

Arg objects can be used to process command line options passed to the main function of the program. A syntax for short-named and long-named options is supported. Short-named options start with a single hypen followd by a character (-O). Optionally, a value follows directly (-Ofoo) or separated by whitespace(-O foo). Alternatively, option names can be consist of any character sequence to support unix style options(–option) and windows style options (/OPTION). An optional value follows either separated by whitespace (–option yes) or an equal character (/OPTION=yes). Note that constructing an Arg from with the character 'n' or with the string "-n" are equivalent.

The template parameter of the Arg class is the argument value type, which must be streamable, i.e. the operator >> (std::istream&, T&) must be defined for the type T. When an Arg is constructed, the operator will be used to extract the value from the command-line string. the next example demonstrates this:

int main(int argc, char* argv[])
{
Pt::Arg<int> option_n(argc, argv, 'n', 0);
std::cout << "value for -n: " << option_n << endl;
}

Options are removed from the option-list, so programs can easily check, if there are parameters left, after all options were extracted. A specialization exists for boolean parameters. This implements a switch, which is on, if the option is present and off, if it is missing. The option consists, in this case, only of a command line flag without a value. Boolean parameters can also be grouped, so -abc is processed like -a -b -c.

Pt::Arg<bool> debug(argc, argv, "--debug");
if (debug)
std::cout << "debug flag is set" << std::endl;

The example shown above not only shows a boolean parameter, but also how long-named options are handled, in this case "--debug".

Application Settings

Many programs need to be able to restore its settings from a persistent location, such as a file. The Settings class provides an hierachical organisation of settings entries and an API to read and write them in a text format. The following example illustrates how settings can be read from a file:

std::ifstream ifs("app.settings");
Pt::TextIStream tis(ifs, new Pt::Utf8codec);
Pt::Settings settings;
settings.load(tis);

Settings can be loaded from any input stream, so the API is not limited to files. In this example, a file stream is opened and a text input stream is used to read UTF-8 encoded text. Another interesting use-case is to load settings from a string stream, which can greatly simplify unit testing. Writing settings to a file is just as easy:

std::ofstream ofs("app.settings", std::ios::out|std::ios::trunc);
Pt::TextOStream tos(ofs, new Pt::Utf8codec);
Pt::Settings settings;
settings.save(tos);

Any output stream can be used to save the settings, in this case UTF-8 encoded text is written to a file. Note, that the file is truncated when opened, so the content is replaced.

Settings are saved in a compact text format, which supports integers, floats, strings and booleans as scalar value types and arrays and structs as compound types. The next example shows some possibilities:

a = 1
b = 3.14
c = "Hello World!"
d = true
e = [ 1, 2, 3 ]
f = { red = 255, green = 0, blue = 0 }

The entry values for a, b, c and d are of type integer, float, string and bool, respectively. The entries e and f demonstrate the syntax for arrays and structs. The following example shows how such a settings file can be loaded and how the entries are accessed:

std::ifstream ifs("app.settings");
Pt::TextIStream tis(ifs, new Pt::Utf8codec);
Pt::Settings settings;
settings.load(tis);
int a = 0;
bool ok = settings["a"].get(a);
float b = 0;
ok = settings.entry("b").get(b);
ok = settings.entry("c").get(c);
bool d = false;
ok = settings.entry("d").get(d);
std::vector<int> e;
ok = settings.entry("e").get(e);
Color f;
ok = settings.entry("f").get(f);

The entry() method or alternatively, the index operator can be used, to access entries and subentries by name. If a subentry does not exist, an empty entry object will be returned. Values can be retrieved with the get() method, which returns false, if the value does not exist. The data type, which is stored in the settings must be serializable i.e. the serialization operators must be defined. The framework defines the serialization operators for STL containers, so these work out of the box. The set() function can be used to set an entry to a new value, before the modified settings are saved. New subentries can be added using the addEntry() function.

Settings can be split into sections, to improve the readability of the file, using the following syntax:

[animals]
a = "dog"
b = "cat"
[plants]
a = "tulip"
b = "rose"

When such a settings file is loaded, it will contain two entries named "animals" and "plants". Both entries will have two subentries named "a" and "b".

Fixed-Size Integers

The Pt framework defines a number of fixed-size, signed and unsigned integers ranging from 8-bit to 64-bit widths. They are typedefs for builtin fundamental types such as int or long and the actual type depends on the platform. For example, Pt::uint8_t is a typedef for an unsigned 8 bits wide integer type and Pt::int32_t is a typedef for a signed 32 bits wide integer. The following table shows all available fixed-size integer types:

int8_t signed 8 bit integer uint8_t unsigned 8 bit integer
int16_t signed 16 bit integer uint16_t unsigned 16 bit integer
int32_t signed 32 bit integer uint32_t unsigned 32 bit integer
int64_t signed 64 bit integer uint64_t unsigned 64 bit integer

Atomic Operations

Pt::atomic_t and the atomicGet(), atomicSet(), atomicIncrement(), atomicDecrement(), atomicExchange() and atomicCompareExchange() functions perform lock-free integer and pointer operations with acquire or release memory ordering; they are the building block for the higher-level concurrency primitives of Concurrency.

Dates and Times

Pt::Timespan, Pt::Date, Pt::Time and Pt::DateTime represent durations and points in calendar time.

Timespan

Time intervals can be represented by Pt::Timespan objects with microsecond accuracy. It is often the result of the calculations involving Pt::Date, Pt::Time ot Pt::DateTime. A Pt::Timespan can be constructed from the number of microseconds and then be converted to other time units with toHours(), toSeconds(), toMSecs() and toUSecs(). When two Pt::Timespans are compared, the shorter one is considered less. Addition and subtraction is supported as shown in the next example:

#include <Pt/Timespan.h>
#include <iostream>
Pt::Timespan ts1(10000);
Pt::Timespan ts2(1000);
Pt::Timespan d = ts2 - ts1;
std::cout << "The difference is " << d.toSecs() << " secs" << std::endl;

Date

Pt::Date is an easy way to handle calendar dates. It can be constructed from the days, month and year components or from an ISO string using fromIsoString(). The date components can be accessed with day(), month() and year(). Once a Date object is created, calendar information can be accessed, for example with dayOfYear(), dayOfWeek() or isLeapYear(). Dates can be compared and a Date is considered less if it is earlier than another Date. It is also possible to add or subtract days from a Date, which yields a new date or modifies it. Dates can be subtracted, which yields the number of days between them, as shown in the next example:

#include <Pt/Date.h>
#include <iostream>
int daysUntilChristmas(const std::string& todayStr)
{
Pt::Date today = Pt::Date::fromIsoString(todayStr);
Pt::Date xmas(today.year(), 12, 24);
return xmas - today;
}

Pt::InvalidDate is thrown, if a date could not be constructed, for example if one of the date components is out of range. To avoid an exception, date components can be validated with isValid().

Time

A Pt::Time object contains a wall-clock time in hours, minutes, seconds amd milliseconds. It can be constructed either from the numeric time values or from a string in ISO format using fromIsoString(). The separate time values can be accessed with hour(), minute(), second() and msec(). Times can be compared and a Time is considered less, if it is earlier than another Time. Subtracting a time from another yields a Pt::Timespan as the result. Pt::Timespans can also be added or subtracted from a Time, yielding a new Time or modifying it, as shown in the following example:

#include <Pt/Date.h>
#include <iostream>
Pt::Timespan untilMidnight(const std::string& timeStr)
{
Pt::Date midnight(0 ,0, 0);
return midnight - now;
}

Pt::InvalidTime is thrown, if a time could not be constructed, for example if one of the time values is out of range. To avoid an exception, time values can be validated with isValid().

DateTime

Pt::DateTime combines a Pt::Date and a Pt::Time object into one instance. It can either be constructed from the corrsponding numeric values or a string in ISO format. The date and time parts can be accessed with date() and time(). When two DateTimes are compared, one is considered less, if its date is earlier or if the time is earlier in case of equal dates. A Pt::Timespan can be added or subtracted from a DateTime and this is also the result when two DateTimes are subtracted. To avoid the exceptions thrown by the underlying time and date, isValid() can be used to check numeric date and time values.

Coroutines

A Task is a C++20 coroutine that produces a single result and can be cancelled while it is suspended. A Generator yields a sequence of values and may also suspend. Both types integrate with the event loop: awaitable operations resume the coroutine when I/O, timers or other loop-driven work complete.

These types are available when the program is compiled as C++20 or later.

Use a function that returns Task as an asynchronous unit of work. The coroutine starts suspended. Use Task::run() to begin execution on the current thread. After the task has finished, Task::done() is true and Task::result() returns the value.

Inside a task, co_await suspends until an awaitable operation completes. Framework types already provide awaitables, for example Pt::System::Timer::waitAsync() for a one-shot delay. Custom operations derive from Awaiter or BasicAwaiter.

I/O awaitables typically resume from the event loop. The usual pairing is to start the task with Task::run() and then run the loop so completions can resume the coroutine.

Destroying a task or calling Task::cancel() aborts the pending awaitable and destroys the coroutine frame. Nested tasks cancel the inner pending operation the same way. A generator cancels in the same manner.

Exceptions that leave a coroutine body are stored. They are rethrown by Task::result() and by co_await of that task.

The following example starts a task that waits for a timer and then exits the loop:

Pt::Task<> delayThenExit(Pt::System::EventLoop& loop,
{
co_await timer.waitAsync(1000);
loop.exit();
}
int main()
{
timer.setActive(loop);
Pt::Task<> task = delayThenExit(loop, timer);
task.run();
return loop.run();
}

Tasks

A Task is move-only and owns the coroutine frame. A default-constructed task is empty; Task::operator bool() is false until a coroutine is assigned. Move assignment cancels the current task first.

Task<void> produces no value. Task<T&> returns a reference; that object must outlive the consumer.

An outer coroutine can co_await an inner Task. The co_await expression is the inner result:

{
co_return 41;
}
{
int n = co_await inner();
co_return n + 1;
}

Task::run() and awaiting a task that is already pending throw std::logic_error.

Awaitables

Derive from Awaiter to wrap an asynchronous operation so a Task or Generator can suspend until it completes. Implement Awaiter::onBegin() to subscribe to completion and start the work. Implement Awaiter::onCancel() to abort that work. Call Awaiter::setReady() when the operation finishes; that resumes the waiting coroutine.

Implement await_resume() in the subclass to deliver the result, or derive from BasicAwaiter when the awaitable only needs to produce a value through BasicAwaiter::onReady().

The following awaiter starts a device operation and resumes when the device signals completion:

class AsyncOp : public Pt::Awaiter
{
public:
explicit AsyncOp(Device& device)
: _device(device)
{}
int await_resume()
{
return _device.endOp();
}
protected:
void onBegin() override
{
_device.finished() += Pt::slot(*this, &AsyncOp::setReady);
_device.beginOp();
}
void onCancel() override
{
_device.cancel();
}
private:
Device& _device;
};

Use BasicAwaiter instead of Awaiter when the awaitable only needs to produce a value, or to complete with no value. Subclasses still implement Awaiter::onBegin() and Awaiter::onCancel(). Implement BasicAwaiter::onReady() to return the result. The co_await expression is that return value.

BasicAwaiter<void> is the specialization for operations that do not produce a result. Its BasicAwaiter::onReady() returns nothing and can finalize or clean up the operation.

Generators

A Generator produces values lazily with co_yield. Unlike a synchronous generator, its body may also co_await.

Consume a generator from a Task. Await Generator::next() until it returns false and read each value with Generator::value().

A Generator is move-only and owns the coroutine frame. Generator<T&> yields a reference. That object must remain valid until the next Generator::next() or until the generator is destroyed.

Only one Generator::next() may be pending. Awaiting next() while another next() is already pending throws std::logic_error.

Exceptions that leave the coroutine body are rethrown from the Generator::next() await.

The following task sums the values of a generator:

Pt::Generator<int> squares(int n)
{
for(int i = 1; i <= n; ++i)
co_yield i * i;
}
Pt::Task<int> sumSquares(int n)
{
auto gen = squares(n);
int sum = 0;
while( co_await gen.next() )
sum += gen.value();
co_return sum;
}

Allocators

The Pt::Allocator interface can be used to optimize or customize allocation strategies. Two allocators are provided, which can be approached by the Allocator interface, a pool based allocator and a page based allocator. A pool based allocator is beneficial in all cases where many small objects of small sizes are created. This is for example used to optimize memory usage during serialization. The page based allocator simply places data consecutively in memory and frees the whole block when its no longer in use. This is useful in situations where chunks of memory or objects are created and destroyed at the same time.

The Allocator Interface

Allocators allow a program to use different methods of allocating and deallocating raw memory. The default implementation will simply use new and delete. Custom allocators are implemented by overriding the two methods allocate() and deallocate() of the Pt::Allocator base class. The following example tracks the amount of allocated memory:

class CheckedAllocator : public Pt::Allocator
{
public:
CheckedAllocator()
: _allocated(0)
{}
virtual void* allocate(std::size_t size)
{
void* p = Pt::Allocator::allocate(size);
_allocated += size;
return p;
}
virtual void deallocate(void* p, std::size_t size)
{
_allocated -= size;
}
std:size_t allocated() const
{ return _allocated; }
private:
std::size_t _allocated;
};

This interface differs from std::allocator used for STL containers, because it allows to allocate memory of different sizes through the same interface. The std::allocator is meant to allocate and also construct objects of the same size. It is however possible, to implement a std::allocator using the raw memory allocators described here.

Pool Allocation

The PoolAllocator uses pools to allocate memory. Each pool consists of blocks of equally sized records, which can be used for allocations up to the size of a record. The record sizes increase from pool to pool. When memory is allocated, a record is used from the pool, which handles the requested size. When memory is deallocated, the record is returned to the corresponding pool. This method of allocation is effective, because larger blocks of memory are allocated and then reused in the form of many smaller records. An advantage of this kind of allocator, compared to free list based allocators, is that it is able to release completely unused blocks.

// Contruct with max. record size, alignment and block size
Pt::PoolAllocator allocator(32, 8, 4096);
// will use a record from the pools
void* p1 = allocator.allocate( sizeof(float) );
// too large, will use operator new
void* p2 = allocator.allocate( 64 );

When a PoolAllocator is constructed, the maximum size for records has to be specified. The reason for this is that this type of allocator is ineffective for large allocations. Therefore, memory which is larger than this limit will be allocated using the new operator, instead of a record from a memory pool. Optionally, the alignment and the maximum block size can be set. The record sizes of the pools will be multiples of the alignment. So if the alignment is 8, the first pool will have records of size 8, the second pool records of size 16 and so forth, until the maximum size is reached. The maximum block size controls the number of records per block. A new block of records is added, when a pool is depleted and has to be extended to allow more allocations.

If memory of uniform sizes has to be allocated, a MemoryPool can be used directly, rather than indirectly as part of the PoolAllocator. This can be faster, because the PoolAllocator has to look up the pool for the requested size of memory each time it allocates and deallocates. To construct a MemoryPool, the size of the records, i.e. the size of memory it can allocate, has to be specified.

Pt::MemoryPool pool( sizeof(float), 4096 );
void* p = pool.allocate();
float* f = new (p) float(3.1415);
pool.deallocate(f);

Optionally, the maximum size of the blocks in the pool can be controlled. In the example shown above, the pool can only allocate memory of the size required for a float. Each time the pool itself requires more memory, it will allocate a new block of 4096 bytes.

Page Allocation

The PageAllocator is useful, when chunks of memory have to be allocated, that can be released simultaneously. This allows the PageAllocator to allocate memory consecutively on pages and simply release all pages together at the end. Therefore, deallocate() will not do anything, but memory will only eventually be released, when clear() is called or the PageAllocator is destructed. The next example illustrates this:

void useAllocator(Pt::Allocator& a)
{
for(std::size_t n = 1; n < 16; ++n)
{
void* p = a.allocate(n);
...
// won't do anything if it's a PoolAllocator
a.deallocate(p, n);
}
}
useAllocator(allocator);
// release all allocated memory
allocator.clear();

Text Processing

This set of classes and functions extends the string and localization support of the C++ standard library to work with unicode characters and strings. A unicode character type and string class (a specialization of std::basic_string) can be used to hold unicode text. A set of functions allows to transform and classify individual characters. Text can be converted e.g. between different encodings using i/o streams and text codecs. A regular expression class allows to search and match patterns in unicode strings. Localization facets are available for the systems which support standard C++ locales.

One of the most common standards for character encoding is the ASCII standard. Each character is encoded using 7 bits of a byte, so 128 different characters can be addressed. Reading and writing ASCII characters is straightforward, because each character is stored in exactly one byte. The builtin C++ type char can be used to represent ASCII characters. The draw-back of ASCII, of course, is the small character set of only 128 characters. There are a lot more characters than that in the languages all around the world.

This problem was addressed by the Unicode standard, which was created to make every known character of the world available in a single character table. Each character has a defined position in the table, a so-called code point. The unicode table contains 0x10FFFF entries at the moment, so a 32 bit type is required to represent a raw unicode character.

The UTF-8 encoding was introduced to store unicode characters in byte sequences, which are compatible to classic null-terminated C strings. One unicode character is encoded into a byte sequence of 1 or more bytes. Further, the characters are encoded such that a character in 7-bit ASCII has the exact same value as in UTF-8, so any valid ASCII text is valid UTF-8 encoded text. This demonstrates the difference between encodings and character types. ASCII and UTF-8 can both be represented by sequences of the character type char, but their values are interpreted according to the encoding. Besides UTF-8 encoding, many more encodings have been developed, for example Latin-1, UTF-16 or, in the broadest sense, Base64.

Characters and Strings

The unicode character type Pt::Char can directly represent a unicode code point. It is used as the character type for Pt::String or Pt::StringStream. Characters can be classified or transformed using a set of functions similar to what can be found in the cctype header of the standard library:

Pt::Char ch = 'a';
// check character category
assert( isalpha(ch) );
assert( islower(ch) );
// convert to upper case
Pt::Char ch2 = toupper(ch);
assert( isupper(ch) );

This class Pt::String is not yet another unicode string class, but it is a specialization of the std::basic_string template for the unicode character type Pt::Char:

typedef std::basic_string<Pt::Char> String;

It offers all the functionality of the std::basic_string template. This has the advantage, that all generic algorithms that work with std::basic_string should also work with Pt::String. Please refer to a standard c++ manual for a complete overview. Additional methods make it easier to work with other character types. For example, the relational operators are also overloaded for char and wchar_t.

Since a specialization of std::char_traits is also provided, the C++ iostreams can be instantiated for Pt::Char, including the string streams. Three typedefs provide shorter names for the unicode capable string streams:

typedef std::basic_istringstream<Pt::Char> IStringStream;
typedef std::basic_ostringstream<Pt::Char> OStringStream;
typedef std::basic_stringstream<Pt::Char> StringStream;

The insertion and extraction operators (<< and >>) for iostreams require certain localization facets to be present in the std::locale. Pt will install specializations of std::num_put, std::num_get, std::numpunct and std::ctype for Pt::Char. This means that all other facilities that use localization facets will also work.

Text Streams and Codecs

A Pt::TextCodec is used by text converters to encode and decode external byte sequences, hence the name codec. It implements the std::codecvt facet interface, on systems provide the std::locale facilities. Codecs are stateless, which means that one codec can be used with multiple text converters. A TextCodec is constructed with a reference counter that indicates whether the converter or locale manages the lifetime of the codec. If that value is 0, as it is the case if the TextCodec is default constructed, the text converter or locale will delete the codec.

Therefore, a default constructed TextCodec has to be cretaed with new, as it is the rule for all localization facets. This can be avoided by passing a value different from 0 to the codecs constructor, in which case the codec must exist at least as long as the stream that uses it:

Pt::Utf8Codec codec(1);
Pt::TextOStream tos(&codec);

This stream decodes an external character sequence using a codec. Reading from the stream will convert from the the encoding of external characters.

Text streams do not only convert between text encodings, but also between character types of different size. The first template parameter CharT is the character type of the decoded text and the second one ByteT is the character type of the encoded text. They are also called the internal and external character types and may be of the same type. The internal character type is used as the character type of the standard C++ stream base class.

A text stream always works with another stream as input or output. This stream works with another std::basic_istream to read the encoded input, using the external character type. A text stream can be constructed with an underlying stream and a codec, but both can also be set or reset later. If no codec is set, the stream will directly assign characters, instead of converting them. If no target stream is set, the text stream will always be EOF.

The following example demonstrates how a string stream is used as the input for a text stream, which uses a Pt::Utf8Codec to decode UTF-8 encoded text:

std::istringstream iss("UTF-8 encoded text");
std::getline(tis, s);

The std::getline() function reads all input into a Pt::String. The extraction operator can also be used, for example to directly read numbers from the stream.

This stream encodes an external character sequence using a codec. Writing to the stream will convert the written characters to the external character types in the external encoding.

The following example shows how to encode text to an UTF-8 byte sequence:

std::ostringstream oss;
Pt::String s = L"Hello World!";
tos << s;
tos.flush();

The string stream serves as the output of the text stream, which uses a Pt::Utf8Codec to encode text to UTF-8. The insertion operator can be used for strings or to format numbers. When all data has been written to the text stream, flush() needs to be called to finish off the output byte sequence. This is especially important for encodings with shift states.

Base-64 Encoding

The base-64 encoding scheme is not a character encoding in the classical sense, but works very similar to other types of encodings. Base64Codec can be used with the basic text stream templates, where the internal and external character types are both char. The following example shows how text is converted to base-64:

std::ostringstream oss;
BasicTextOStream<char, char> b64(oss, new Base64Codec());
b64 << "Hello World!";
b64.flush();

The string stream serves as the output for the base-64 encoded text. It is important to terminate the output sequence by calling flush(), because the base-64 format requires padding at the end. Inserting std::endl also terminates the base-64 sequence.

Regular Expressions

The Pt::Regex class allows to match a string pattern in unicode text. It resembles the std::basic_regex class and can be used to support systems, where std::basic_regex is not available in the standard C++ implementation. The syntax for the match pattern is similar to the extended POSIX syntax. The following table shows the special characters that can be used to write regular expressions:

. Any character
[ ] A character in a given set
[^ ] A character not in a given set
^ Begin of line
$ End of line
\< Begin of a word
\> End of a word
( ) A marked subexpression
* Matches the preceding element zero or more times
? Matches the preceding element zero or one time
+ Matches the preceding element one or more times
| Matches either the expression before or after the operator
\ Escapes the next character

The regular expression is constructed from a unicode string, either a Pt::String or a null-terminated sequence of unicode characters of type Pt::Char. It can then be used to match it against unicode strings as shown in the next example:

Pt::String expr = L"[hc]ats";
Pt::Regex regex(expr);
Pt::String str1 = L"I like cats!";
Pt::String str2 = L"I like hats!";
Pt::String str3 = L"I like bats!";
// this does match
bool matched = regex.match(str1);
// this does also match
matched = regex.match(str2);
// this does not match
matched = regex.match(str3);

It is also possibe to match a regular expression against a unicode input string and find out what tokens in the string actually matched. The match() member function has an overload, which fills a Pt::RegexSMatch with the result. Note that the first result at index 0 is always the input string itself. The following example illustrates this:

Pt::String expr = L"([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)";
Pt::Regex regex(expr);
Pt::String str = L"My IP address is 192.168.0.77";
bool matched = regex.match(str, smatch);
if(matched)
{
std::cout << "IP: " << smatch.str(1).narrow() << std::endl;
}
else
{
std::cout << "No IP in " << smatch.str(0).narrow() << std::endl;
}

Signals and Delegates

Callback mechanisms for event handling have become ubiqitous in todays application frameworks. Older examples are the use of function pointers as callbacks or the message maps found in the MFC toolkit. More modern approaches include the .NET delegates and so-called signal-slot techniques. When signals and slots are used, objects can communicate with each other by connecting a signal of one object to the slot of another object. In most cases connection management features are built-in so that an object closes all it connections automatically when it gets destroyed. Once the connection has been established, all connected slots are called when a signal is send. Connecting signals to slots is type-safe i.e. a signal can only be connected to a slot that matches the signal's signature. At the same time it allows a great deal of flexibility (loose coupling), since the caller has no intimite knowledge of the callee. A simple but real example might look like this:

int main()
{
timer.setActive( app.loop() );
timer.start(1000);
timer.timeout() += Pt::slot(app, &Pt::System::Application::exit);
return app.run();
}

This program will simply exit, when the timer expires after 1000 ms. The application object is the callee and the member function Application::exit serves as a slot. The timer is the caller, which has a signal called timeout.

Non-generic lambdas and function objects derive their slot signature from operator(). Generic lambdas use an explicitly stated slot signature. The callable must be copyable because a connection clones its slot. A lambda slot cannot be removed with operator-= because captured values do not have general equality. Retain the returned Connection and close it when no context is supplied.

Pt::Connection connection = signal +=
Pt::slot([this](int) { add(42); });
delegate += Pt::slot<int, int>([](auto value) { return value + 1; });
int result = delegate.call(41);
connection.close();
Pt::Connectable context;
signal += Pt::slot(context, [this](int) { add(42); });

Destroying the context closes its bound connections automatically. The context must outlive every unsafe capture while the connection remains open.

Signals

Signals are normally members of objects and are being sent e.g. When the object state changes or some event occurs. When a signal is sent, it calls all slots it is connected to. Callable entities, like functions or member functions can serve as slots for signals. The template parameter list of the Pt::Signal class template determines the signature of the signal. If a signal does not have any arguments the parameter list is left empty:

Pt::Signal<> sig0; // Signal without arguments
Pt::Signal<int> sig1; // Signal with one argument
Pt::Signal<int, int> sig2; // Signal with two arguments

Slots can be constructed with the slot() function, which is overloaded for various types of callable entities, most notably functions or member functions. Slots are lightweight proxy-objects and one example is the Pt::MethodSlot, which allows to use a member function as a slot.

A signal can be connected to a slot if the signatures are compatible. One important feature of Pt::Signal is that the return value of a slot is ignored and therefore a slot is compatible to a signal no matter what type it returns. The following code example shows how a signal is connected to a function and a member function:

class Callee : public Pt::Connectable
{
public:
void slot()
{ std::cout << "Callee::slot() called" << std::endl; }
};
void slot()
{ std::cout << "slot() called." << std::endl; }
int main()
{
Callee callee;
Pt::Signal<> signal;
signal += Pt::slot(slot);
signal += Pt::slot(callee, &Callee::slot);
return 0;
}

Two slots are constructed, one from a function pointer and another one from a member function pointer and the object instance to be called. The signal is connected to both slots. Signals can only be connected to objects that derive from Pt::Connectable, to ensure that all connections are closed when the object runs out of scope and no dangling connections are left. The += operator, to connect a signal with a slot, returns a connection object, which can be used to disconnect signals from slots manually. The following code illustrates this:

void slot()
{ std::cout << "slot() called." << std::endl; }
int main()
{
Pt::Signal<> signal;
Connection c = signal += Pt::slot(slot);
c.isValid() // returns true
c.close();
c.isValid() // returns false
return 0;
}

A connection is reference counted and can not be duplicated as such, but always refers to the same shared connection data. If one peer of a connection is destroyed or the connection is closed manually, the connection becomes invalid. Once a connection has been established, signals can be send to invoke the connected slots. This happens by calling send() with the appropriate arguments, if any.

void tellAge(int age)
{ std::cout << "I am " << age << " years old\n"; }
int main ()
{
signal += Pt::slot(tellAge);
signal.send(26);
return 0;
}

When the signal is send, the slot is called with the same value passed to Signal::send. Nothing will happen if the signal is not connected to any slots. When a signal is sent, the slot is called immediatly and directly and does not depend on an event loop. If multiple slots are connected to a signal, the slots will be called one after another.

Delegates

The Pt::Delegate is an alternative to the Pt::Signal and differs from it in two ways. Firstly, delegates forward the return value of the slot and secondly, delegates can only be connected to one slot at a time. The same types of slots can be used for signals and delegates. The template parameter list of the Delegate determines its signature, where the first parameter represents the return type:

Pt::Delegate<int> del0; // Delegate only returns int
Pt::Delegate<int, int> del1; // Delegate with one argument
Pt::Delegate<int, int, int> del2; // Delegate with two arguments

The syntax for connecting delegates is identical to how signals are connected to slots. However, since a delegate forwards the return value of its slot, not only the arguments passed to the slot must be compatible, but also the return value. Furthermore, when an already connected delegate is connected again, the current connection will be closed and the delegate is connected to its new target.

int slotA()
{ return 5; }
int slotB()
{ return 6; }
int main()
{
delegate += Pt::slot(slotA);
delegate += Pt::slot(slotB); // disconnects from slotA
return 0;
}

The example above constructs a delegate which can be connected to any slot that returns an int. It is first connected to a slot of the function slotA. When it is connected for the second time to a slot of the function slotB, the old connection will be closed and only slotB is called when the delegate is called.

There are two possibilities how a delegate can call its slot. The member function call() will return the return value of the slot. If the delegate is not connected to a slot, an exception is thrown. The second method is through invoke(), where the return value is ignored, but if the delegate is not connected, no exception will be thrown.

int slot()
{ return 5; }
int main()
{
try
{
Pt::Connection connection = delegate += Pt::slot(slot);
int i = delegate.call(); // i is 5 now
connection.close();
delegate.invoke() // does not throw
delegate.call(); // will throw because not connected
}
catch(const std::logic_error& ex)
{
std::cerr << "could not call delegate" << std::endl;
}
return 0;
}

The program above connects a delegate to a slot and then calls it. After the connection was closed, the delegate is invoked, which has no effect. Finally, when the delegate is called again, an exception is thrown and catched.

Type Traits and Information

Two types are useful to get type information: Pt::TypeTraits and Pt::TypeInfo. TypeTraits are used for generic programming, for example to deduce the pointer type in templated code, or to branch differently for const and non-const types. The TypeInfo class is a wrapper for std::type_info, which makes it easier to store and compare type information. The std::type_info is normally not copyable and comparable. Pt::SourceInfo is used to store information about a location in the source code.

Type Information

The normal std::type_info class is not copyable, so only raw pointers can be stored in containers such as std::vector. The TypeInfo class addresses this problem, by wrapping std::type_info into a type with value semantics. It also adds comparison operators, like less-than comparison, which allow to use it as the key for assoziative containers:

// OK, TypeInfo is copyable
std::vector<Pt::TypeInfo> typeVector;
// OK, TypeInfo is less-than comparable
std::map<Pt::TypeInfo, std::string> typeMap;

Conversions

The framework includes functions for fast conversion between strings and numbers. The overloaded functions Pt::parseInt() and Pt::formatInt() convert between strings and integers, and Pt::parseFloat() and Pt::formatFloat() convert between strings and floats. The functions work with iterators as input or output instead of string objects, so they can be used with simple buffers or even streams, as shown in the following example:

#include <Pt/Convert.h>
#include <iterator>
#include <iostream>
std::ostream_iterator<char> it(std::cout);
Pt::formatInt(it, 42);
const char* buf = "42";
const char* bufend = buf + 2;
int n = 0;
Pt::parseInt(buf, bufend, n);

A stream iterator is used to format a number directly to std::cout and then a number is parsed from a raw character buffer. Floating point numbers can be formatted and parsed in a similar way like integers:

#include <Pt/Convert.h>
#include <iterator>
#include <iostream>
std::ostream_iterator<char> it(std::cout);
Pt::formatFloat(it, 42.123);
const char* buf = "42.123";
const char* bufend = buf + 6;
float f = 0;
Pt::parseFloat(buf, bufend, f);

By default, decimal format is used when numbers are parsed and formatted, but overloads exist that accept an additional format object. A few format objects are already provided by the framework, named Pt::DecimalFormat, Pt::OctalFormat, Pt::HexFormat and Pt::BinaryFormat, which allow numeric conversion in a different base. The next example shows how integers in hex format can be parsed and formatted:

#include <Pt/Convert.h>
#include <iterator>
#include <iostream>
Pt::HexFormat<char> fmt;
std::ostream_iterator<char> it(std::cout);
Pt::formatInt(it, 0x42, fmt);
const char* buf = "0x42";
const char* bufend = buf + 4;
int n = 0;
Pt::parseInt(buf, bufend, n, fmt);

All parse functions used so far throw an exception of type Pt::ConversionError, if the conversion failed. Overloads of the parse functions are available, which set a bool flag instead, to indicate a conversion error. Both, the format and parse functions return an iterator pointing to the position after the last character that was written or read, respectively. Partial consumption of the input is not treated as an error.

Numeric assignments can lead to loss of data, if the operation narrows the data type to a smaller one. For example, numeric conversion from int to short can be an error, if the assigned value exceeds the maximum or minimum value possible for shorts. Pt::narrow() can be used instead of a normal assignment, to protect against this. In case of an error, an exception of type Pt::ConversionError is thrown.

#include <Pt/Convert.h>
#include <limits>
#include <iostream>
long l = ...;
short s = 0;
try
{
s = Pt::narrow<short>(l);
}
catch(const Pt::ConversionError& e)
{
std::cerr << "numeric value is out of range: " << l << std::endl;
}

The byte order conversion API consists of two sets of functions. Pt::swab() swaps the byte order of a type by bytewise copying, and is overloaded for all fixed-size integer types such as Pt::uint32_t:

#include <Pt/Byteorder.h>
Pt::uint32_t value = ...;
Pt::uint32_t swapped = Pt::swab(value);

A second set of functions can be used to convert from a specific external byte order to the native host byte order: Pt::beToHost(), Pt::hostToBe(), Pt::leToHost() and Pt::hostToLe(). For example Pt::beToHost() converts from big-endian to the host byte order:

#include <Pt/Byteorder.h>
Pt::uint32_t beVal = ...;
Pt::uint32_t value = Pt::beToHost(beVal);

Serialization

Data structures and types can be serialized to text or binary formats using Pt's serialization. This is used within the framework to load and store data or to implement remote procedure calls. It is extensible to work with all kinds of types, including STL containers, PODs (plain old data types), builtin language types or custom data types. The framework separates the process of composing and decomposing types from the formatting stage, resulting in a two-phase serialization process. This also allows to resolve and fixup shared pointers or references.

A type is serializable, if two operators are implemented to compose and decompose it to a SerializationInfo. The SerializationContext provides improved memory management, a mechanism to generate IDs for shared pointers and a way to further customize or override serialization for a type. Alternatively, performance can be increased by implementing a Composer or Decomposer for the type, however it is more complicated to do so.

Various formats are supported by implementing Formatters. Other modules of the framework also implement Formatters, for example to support serialization to XML. The Serializer and Deserializer combine a Formatter and a SerializationContext, manage composition and decomposition and thus form the high-level interface for the serialization of a set of types.

The Pt framework already provides serialization support for the C++ builtin types and the types provided by the C++ standard library, like std::string or std::vector. To make custom types serializable, the serialization operators have to be implemented. The next example shows a simple data type and the declarations of the serialization operators:

struct Address
{
Address()
: code(0)
{}
std::string country;
std::string city;
std::string street;
unsigned code;
};
void operator >>=(const Pt::SerializationInfo& si, Address& address);
void operator <<=(Pt::SerializationInfo& si, const Address& address);

Similar to the insertion and extraction operators for standard C++ iostreams, one operator has to be overloaded to serialize a type and another one to deserialize it. The types of operators (<<= and >>=) indicate that this is an assignment operation. Each type has to be composed from or decomposed to Pt::SerializationInfo objects, which form a tree representing the object graph. It contains all meta information so composition and decomposition can be separated from formatting and parsing. Building up the tree is highly optimized and, for example, requires only very few allocations. The next example shows the definition of the serialization operator:

void operator<<=(Pt::SerializationInfo& si, const Address& address)
{
si.addMember("country") <<= address.country;
si.addMember("city") <<= address.city;
si.addMember("street") <<= address.street;
si.addMember("code").setUInt32(address.code);
si.setTypeName("Address");
}

The SerializationInfo passed by reference to the operator is meant to represent an Address object in the object graph. SerializationInfo child nodes are added for each member variable using Pt::SerializationInfo::addMember(), which also assigns the name. Member types can be serialized to the returned SerializationInfo using their specific overload of the operator. For builtin integer types it is recommended to use a setter (here Pt::SerializationInfo::setUInt32()) instead of the serialization operator, to be specific on the type. Integer types could potentially be serialized differently depending on the platform. For example, a long could be serialized as a 32-bit or a 64-bit integer type. Finally, the type name is set for the parent node, representing the Address object.

The deserialization operator performs the same process, just in reverse. The SerializationInfo object passed to it contains the meta information for all members. SerializationInfo child nodes can be obtained by name using Pt::SerializationInfo::getMember() and members can be deserialized with their overloads of the deserialization operators. The following example illustrates this:

void operator>>=(const Pt::SerializationInfo& si, Address& address)
{
si.getMember("country") >>= address.country;
si.getMember("city") >>= address.city;
si.getMember("street") >>= address.street;
si.getMember("code") >>= address.code;
}
Date expressed in year, month, and day.
Definition: Date.h:104
Indicates a failed conversion.
Definition: ConversionError.h:46
void invoke(As... args)
Invoke the slot connected to the Delegate.
Definition: Delegate.h:251
Signal & timeout()
Notifies about interval timeouts.
Definition: Timer.h:161
void operator<<=(SerializationInfo &si, const Date &date)
Serialize a date.
void load(std::basic_istream< Pt::Char > &is)
Loads settings from a input stream.
Represents a cancellable C++20 coroutine that produces a single result.
Definition: Coroutine.h:436
void run()
Starts the loop.
Represents a coroutine that yields a sequence of values and may itself co_await.
Definition: Generator.h:163
void clear()
Releases all memory.
Notifies clients in constant intervals.
Definition: Timer.h:79
int isalpha(const Char &ch)
Checks whether ch is a alphabetic character.
Definition: String.h:147
void exit()
Exits from the contained event loop.
Definition: Application.h:93
Convert between unicode and UTF-8.
Definition: Utf8Codec.h:44
void setTypeName(const std::string &type)
Sets the type name.
EventLoop & loop()
Returns the event loop.
Definition: Application.h:83
ConstMethodSlot< R, ClassT, As... > slot(ClassT &object, R(BaseT::*method)(As...) const)
Returns a slot object for the given object/member pair.
Definition: ConstMethod.h:172
Read and extract command-line options.
Definition: Arg.h:200
Char toupper(const Char &ch)
Convert a character to upper case.
virtual void onBegin()=0
Starts the asynchronous operation.
void save(std::basic_ostream< Pt::Char > &os) const
Saves settings to a output stream.
SerializationInfo & addMember(const std::string &name)
Add a struct member.
Definition: SerializationInfo.h:449
int year() const
Returns the year-part of the date.
Definition: Date.h:503
T beToHost(const T &value)
Converts a value from big-endian to host-byteorder.
Definition: Byteorder.h:254
Pool based allocator.
Definition: PoolAllocator.h:247
virtual void * allocate(std::size_t size)
Allocates size bytes of memory.
Definition: Allocator.h:96
AsyncWait waitAsync(std::size_t ms)
Start a one-shot timer delay as a C++20 awaitable.
int isupper(const Char &ch)
Checks whether ch is upper case.
Definition: String.h:219
bool get(T &value) const
Gets the value.
Definition: Settings.h:417
void setActive(EventLoop &loop)
Sets the used event loop.
static Date fromIsoString(const std::string &s)
Interprets a string as a date-string in ISO-format.
Definition: Date.h:359
Unicode character type.
Definition: String.h:67
const SerializationInfo & getMember(const std::string &name) const
Get a struct member.
Definition: SerializationInfo.h:496
virtual void onCancel()=0
Aborts the in-flight operation.
Store application settings.
Definition: Settings.h:173
Regular Expressions for Unicode Strings.
Definition: Regex.h:174
Pt::String str(std::size_t n=0) const
Returns the nth match.
Connection Management for Signal and Slot Objects.
Definition: Connectable.h:50
void exit()
Stops the loop.
void setUInt32(Pt::uint32_t n)
Set to 32-bit unsigned integer value.
void send(As... args)
Invlokes all slots.
Definition: Signal.h:237
int8_t swab(int8_t value)
Swaps the byteorder of an int32_t.
Definition: Byteorder.h:133
Result of a regular expression match.
Definition: Regex.h:226
uint_type uint32_t
Unsigned 32-bit integer type.
Definition: Api-Types.h:48
void operator>>=(const SerializationInfo &si, Date &date)
Deserialize a date.
void run()
Starts the contained event loop.
Definition: Application.h:88
OutIterT formatFloat(OutIterT it, T d, const FormatT &fmt, int precision, bool fixed=false)
Formats a floating point value in a given format.
Definition: Convert.h:733
Represents time spans in microsecond resolution.
Definition: Timespan.h:63
Allocator interface.
Definition: Allocator.h:82
Thread-safe event loop supporting I/O multiplexing and Timers.
Definition: EventLoop.h:83
ConstEntry entry(const std::string &name) const
Returns a top level entry.
Definition: Settings.h:591
InIterT parseFloat(InIterT it, InIterT end, T &n, const FormatT &fmt, bool &ok)
Parses a floating point value in a given format.
Definition: Convert.h:942
std::string narrow(char dfault='?') const
Narrow string to 8-bit.
OutIterT formatInt(OutIterT it, T i, const FormatT &fmt)
Formats an integer in a given format.
Definition: Convert.h:635
Page based allocator.
Definition: PageAllocator.h:77
Unicode capable basic_string.
Definition: Api-String.h:63
void close()
Closes the connection.
Console applications without a GUI.
Definition: Application.h:55
void run()
Starts execution of the coroutine.
Definition: Coroutine.h:518
Thread-safe event loop supporting I/O multiplexing and Timers.
Definition: MainLoop.h:67
static Time fromIsoString(const std::string &s)
Convert from an ISO time string.
Definition: Time.h:273
Represents arbitrary types during serialization.
Definition: SerializationInfo.h:59
R call(As... args)
Calls the slot connected to the Delegate.
Definition: Delegate.h:236
Delegates an action to a slot.
Definition: Delegate.h:194
virtual void deallocate(void *p, std::size_t)
Deallocates memory of size bytes.
Definition: Allocator.h:103
Represents a connection between a Signal/Delegate and a slot.
Definition: Connection.h:96
Memory pool for objects of the same size.
Definition: PoolAllocator.h:70
void start(std::size_t interval)
Starts the timer.
InIterT parseInt(InIterT it, InIterT end, T &n, const FormatT &fmt, bool &ok)
Parses an integer value in a given format.
Definition: Convert.h:871
int islower(const Char &ch)
Checks whether ch is lower case.
Definition: String.h:210
Provides the base class for I/O-driven co_await-able operations.
Definition: Coroutine.h:126