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.