C++ signals and slots

vdk-signals is a type-safe and thread-safe signals-slots system for standard C++ designed with performance and simplicity in mind. It follows the main philosophy of the C++ language avoiding unnecessary overheads and superfluous functionality that can slow down your program.

High performance is achieved through the use of modern C++ features and atomic variables. Specialized synchronization mechanism used internally makes signal emissions lock-free and ensures the fastest possible execution. The library supports synchronous and asynchronous slot invocations with automatic detection of target threads, as well as automatic object lifetime tracking.

vdk-signals has no external dependencies and relies on standard C++17 only. It is organized as the amalgamation and can be easily integrated into any existing project.

Overview

There are two main classes in the library: signal and context. Each of them is responsible for some particular tasks and provides interface for internal library's mechanisms.

An instance of signal contains a list of connections (connected slots). Any callable target can be connected to a signal as a slot. When the signal is emitted all connected slots, if any, are executed either synchronously or asynchronously (see below). If a connected slot provides equality comparison operator it can be disconnected in exactly the same way as it was connected: just pass it into disconnect() method. If a slot does not provide such operator (e.g. lambda) it can be disconnected by special connection id value returned from connect() method. Two slots are considered to be equal if they are of exactly the same static type, provide accessible equality comparison operator, and compare equal.

An instance of class inherited from context provides context for slot invocations. Such class obtains two special features: thread affinity and automatic lifetime tracking. Thread affinity means that an object created in a thread belongs to the thread and, by default, receives all signal emissions in that thread. As a consequence, its slots do not need to be thread-safe in a multithreaded environment; all slot invocations will be serialized in the object's thread anyway. This also guarantees that there are no race conditions: once a slot has been disconnected or its context destroyed, the slot will not be reachable for signal emissions anymore.

A context object can also be associated with any callable target, just as if the callable were a member of the object's class. It implies that the callable will be invoked from the thread associated with the context object and disconnected when the context gets destroyed.

Any slot belonging to (or associated with) a context object can be executed synchronously or asynchronously. The default rule is simple: if a signal is emitted from the same thread the context lives in, the execution will be synchronous; otherwise, the execution will be asynchronous. The default behavior may be changed by passing exec::sync or exec::async enumerators as the last argument to the signal's connect() method. If there is no context for a given slot, it will always be executed synchronously.

To make cross thread mechanisms work each thread that creates context objects has a private queue with packaged asynchronous slot invocations transferred to that thread. The only way to access the private thread's queue is by calling signals_execute(). This function extracts and executes packaged slot invocations received so far in the calling thread. Calls to signals_execute() can be easily integrated into an existing event loop provided by your application, if any. Otherwise, it is trivial to write such a loop from scratch (see demos).

The main rule for successful use of cross thread signal emissions is: always execute slot calls and all operations on a context object in the thread the context belongs to. This guarantees the absence of any race conditions and always does the right thing. By default, the library automatically detects what thread a slot should be invoked in, but users should remember to call signals_execute() function in order to invoke asynchronous queued slots.

There is also the lite version of the library designed to be used in a single-threaded environment. It is located in vdk::lite namespace and provides nearly identical interface to its multithreaded counterpart, with the exception that all slots are always executed synchronously.

Usage

Note! vdk-signals requires a compiler that supports C++17 standard.

The library is not meant to be built and linked as a standalone package. Instead, it is organized as "the amalgamation" and contains everything you need in just two files. This allows you to easily integrate vdk-signals into any target project. Just copy signals.h and signals.cpp into your project and compile them together with your other source files.

Please note, that in order to provide maximum flexibility and independence from target project's structure, signals.cpp includes signals.h as a standard header (#include<signals.h>), so make sure that signals.h resides in a folder that is searched for header files by the compiler.

GoogleTest is required to build and run tests. CMake files are provided to simplify the process. If you already have a copy of GoogleTest framework, just run CMake and set GTEST_SOURCE_DIR cache variable to point to the directory that contains your GoogleTest sources. If you don't have GoogleTest sources, CMake will download and compile them automatically.

demo directory contains code examples that can serve as a tutorial for learning how to use vdk::signals.

License

This software is licensed under the Apache License version 2.0.