2026-04-06 00:20:51 -05:00

703 lines
28 KiB
C++

/*
This file is part of KDBindings.
SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
Author: Sean Harmer <sean.harmer@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#pragma once
#include <assert.h>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <forward_list>
#ifdef emit
static_assert(false, "KDBindings is not compatible with Qt's 'emit' keyword.\n"
"To use KDBindings with Qt, please define QT_NO_EMIT to disable Qt's 'emit' keyword.\n"
"If you're using CMake you can set KDBindings_QT_NO_EMIT to ON to add this.");
// Undefine emit to suppress more compiler errors after the static_assert failure.
// Otherwise the compiler would drown our custom error message with more errors.
#undef emit
#endif
#include <kdbindings/connection_evaluator.h>
#include <kdbindings/genindex_array.h>
#include <kdbindings/utils.h>
#include <kdbindings/KDBindingsConfig.h>
/**
* @brief The main namespace of the KDBindings library.
*
* All public parts of KDBindings are members of this namespace.
*/
namespace KDBindings {
/**
* @brief A Signal provides a mechanism for communication between objects.
*
* KDBindings::Signal recreates the <a href="https://doc.qt.io/qt-5/signalsandslots.html">Qt's Signals & Slots mechanism</a> in pure C++17.
* A Signal can be used to notify any number of slots that a certain event has occurred.
*
* The slot can be almost any callable object, including member functions and lambdas.
*
* This connection happens in a type-safe manner, as a slot can only be connected to
* a Signal when the arguments of the slot match the values the Signal emits.
*
* The Args type parameter pack describe which value types the Signal will emit.
*
* Deferred Connection:
*
* KDBindings::Signal supports deferred connections, enabling the decoupling of signal
* emission from the execution of connected slots. With deferred connections, you can
* connect slots to the Signal that are not immediately executed when the signal is emitted.
* Instead, you can evaluate these deferred connections at a later time, allowing for
* asynchronous or delayed execution of connected slots.
*
* Examples:
* - @ref 01-simple-connection/main.cpp
* - @ref 02-signal-member/main.cpp
* - @ref 03-member-arguments/main.cpp
* - @ref 07-advanced-connections/main.cpp
*/
template<typename... Args>
class Signal
{
static_assert(
std::conjunction<std::negation<std::is_rvalue_reference<Args>>...>::value,
"R-value references are not allowed as Signal parameters!");
// The Signal::Impl class exists, so Signals can be implemented in a PIMPL-like way.
// This allows us to easily move Signals without losing their ConnectionHandles, as well as
// making an unconnected Signal only sizeof(shared_ptr).
class Impl : public Private::SignalImplBase
{
public:
Impl() noexcept { }
~Impl() noexcept { }
// Signal::Impls are not copyable
Impl(Impl const &other) = delete;
Impl &operator=(Impl const &other) = delete;
// Signal::Impls are not moveable, this would break the ConnectionHandles
Impl(Impl &&other) = delete;
Impl &operator=(Impl &&other) = delete;
// Connects a std::function to the signal. The returned
// value can be used to disconnect the function again.
Private::GenerationalIndex connect(std::function<void(Args...)> const &slot)
{
Connection newConnection;
newConnection.slot = slot;
return m_connections.insert(std::move(newConnection));
}
// Establish a deferred connection between signal and slot, where ConnectionEvaluator object
// is used to queue all the connection to evaluate later. The returned
// value can be used to disconnect the slot later.
Private::GenerationalIndex connectDeferred(const std::shared_ptr<ConnectionEvaluator> &evaluator, std::function<void(Args...)> const &slot)
{
auto weakEvaluator = std::weak_ptr<ConnectionEvaluator>(evaluator);
auto deferredSlot = [weakEvaluator = std::move(weakEvaluator), slot](ConnectionHandle &handle, Args... args) {
if (auto evaluatorPtr = weakEvaluator.lock()) {
auto lambda = [slot, args...]() {
slot(args...);
};
evaluatorPtr->enqueueSlotInvocation(handle, lambda);
} else {
throw std::runtime_error("ConnectionEvaluator is no longer alive");
}
};
Connection newConnection;
newConnection.m_connectionEvaluator = evaluator;
newConnection.slotReflective = deferredSlot;
return m_connections.insert(std::move(newConnection));
}
Private::GenerationalIndex connectReflective(std::function<void(ConnectionHandle &handle, Args...)> const &slot)
{
Connection newConnection;
newConnection.slotReflective = slot;
return m_connections.insert(std::move(newConnection));
}
// Disconnects a previously connected function
//
// WARNING: While this function is marked with noexcept, it *may* terminate the program
// if it is not possible to allocate memory or if mutex locking isn't possible.
void disconnect(const ConnectionHandle &handle) noexcept override
{
// If the connection evaluator is still valid, remove any queued up slot invocations
// associated with the given handle to prevent them from being evaluated in the future.
auto idOpt = handle.m_id; // Retrieve the connection associated with this id
// Proceed only if the id is valid
if (idOpt.has_value()) {
auto id = idOpt.value();
// Retrieve the connection associated with this id
auto connection = m_connections.get(id);
if (connection && m_isEmitting) {
// We are currently still emitting the signal, so we need to defer the actual
// disconnect until the emit is done.
connection->toBeDisconnected = true;
m_disconnectedDuringEmit = true;
return;
}
if (connection && connection->m_connectionEvaluator.lock()) {
if (auto evaluatorPtr = connection->m_connectionEvaluator.lock()) {
evaluatorPtr->dequeueSlotInvocation(handle);
}
}
// Note: This function may throw if we're out of memory.
// As `disconnect` is marked as `noexcept`, this will terminate the program.
m_connections.erase(id);
}
}
// Disconnects all previously connected functions
//
// WARNING: While this function is marked with noexcept, it *may* terminate the program
// if it is not possible to allocate memory or if mutex locking isn't possible.
void disconnectAll() noexcept
{
const auto numEntries = m_connections.entriesSize();
const auto sharedThis = shared_from_this();
for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
const auto indexOpt = m_connections.indexAtEntry(i);
if (sharedThis && indexOpt) {
disconnect(ConnectionHandle(sharedThis, *indexOpt));
}
}
}
bool blockConnection(const Private::GenerationalIndex &id, bool blocked) override
{
Connection *connection = m_connections.get(id);
if (connection) {
const bool wasBlocked = connection->blocked;
connection->blocked = blocked;
return wasBlocked;
} else {
throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
}
}
bool isConnectionActive(const Private::GenerationalIndex &id) const noexcept override
{
return m_connections.get(id);
}
bool isConnectionBlocked(const Private::GenerationalIndex &id) const override
{
auto connection = m_connections.get(id);
if (connection) {
return connection->blocked;
} else {
throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
}
}
void emit(Args... p)
{
if (m_isEmitting) {
throw std::runtime_error("Signal is already emitting, nested emits are not supported!");
}
m_isEmitting = true;
const auto numEntries = m_connections.entriesSize();
// This loop can *not* tolerate new connections being added to the signal inside a slot
// Doing so will be undefined behavior
for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
const auto index = m_connections.indexAtEntry(i);
if (index) {
const auto con = m_connections.get(*index);
if (!con->blocked) {
if (con->slotReflective) {
if (auto sharedThis = shared_from_this(); sharedThis) {
ConnectionHandle handle(sharedThis, *index);
con->slotReflective(handle, p...);
}
} else if (con->slot) {
con->slot(p...);
}
}
}
}
m_isEmitting = false;
if (m_disconnectedDuringEmit) {
m_disconnectedDuringEmit = false;
// Because m_connections is using a GenerationIndexArray, this loop can tolerate
// deletions inside the loop. So iterating over the array and deleting entries from it
// should not lead to undefined behavior.
for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
const auto index = m_connections.indexAtEntry(i);
if (index.has_value()) {
const auto con = m_connections.get(index.value());
if (con->toBeDisconnected) {
disconnect(ConnectionHandle(shared_from_this(), index));
}
}
}
}
}
private:
friend class Signal;
struct Connection {
std::function<void(Args...)> slot;
std::function<void(ConnectionHandle &, Args...)> slotReflective;
std::weak_ptr<ConnectionEvaluator> m_connectionEvaluator;
bool blocked{ false };
// When we disconnect while the signal is still emitting, we need to defer the actual disconnection
// until the emit is done. This flag is set to true when the connection should be disconnected.
bool toBeDisconnected{ false };
};
mutable Private::GenerationalIndexArray<Connection> m_connections;
// If a reflective slot disconnects itself, we need to make sure to not deconstruct the std::function
// while it is still running.
// Therefore, defer all slot disconnections until the emit is done.
//
// Previously, we stored the ConnectionHandles that were to be disconnected in a list.
// However, that would mean that disconnecting cannot be noexcept, as it may need to allocate memory in that list.
// We can fix this by storing this information within each connection itself (the toBeDisconnected flag).
// Because of the memory layout of the `struct Connection`, this shouldn't even use any more memory.
//
// The only downside is that we need to iterate over all connections to find the ones that need to be disconnected.
// This is helped by using the m_disconnedDuringEmit flag to avoid unnecessary iterations.
bool m_isEmitting = false;
bool m_disconnectedDuringEmit = false;
};
public:
/** Signals are default constructible */
Signal() = default;
/**
* Signals cannot be copied.
**/
Signal(const Signal &) = delete;
Signal &operator=(Signal const &other) = delete;
/** Signals can be moved */
Signal(Signal &&other) noexcept = default;
Signal &operator=(Signal &&other) noexcept = default;
/**
* A signal disconnects all slots when it is destructed
*
* Therefore, all active ConnectionHandles that belonged to this Signal
* will no longer be active (i.e. ConnectionHandle::isActive will return false).
*
* @warning While this function isn't marked as throwing, it *may* throw and terminate the program
* if it is not possible to allocate memory or if mutex locking isn't possible.
*/
~Signal() noexcept
{
disconnectAll();
}
/**
* Connects a std::function to the signal.
*
* When emit() is called on the Signal, the functions will be called with
* the arguments provided to emit().
*
* @return An instance of ConnectionHandle, that can be used to disconnect
* or temporarily block the connection.
*
* @warning Connecting functions to a signal that throw an exception when called is currently undefined behavior.
* All connected functions should handle their own exceptions.
* For backwards-compatibility, the slot function is not required to be noexcept.
*/
KDBINDINGS_WARN_UNUSED ConnectionHandle connect(std::function<void(Args...)> const &slot)
{
ensureImpl();
return ConnectionHandle{ m_impl, m_impl->connect(slot) };
}
/**
* Establishes a connection between a signal and a slot, allowing the slot to access and manage its own connection handle.
* This method is particularly useful for creating connections that can autonomously manage themselves, such as disconnecting
* after being triggered a certain number of times or under specific conditions. It wraps the given slot function
* to include a reference to the ConnectionHandle as the first parameter, enabling the slot to interact with
* its own connection state directly.
*
* @param slot A std::function that takes a ConnectionHandle reference followed by the signal's parameter types.
* @return A ConnectionHandle to the newly established connection, allowing for advanced connection management.
*
* @warning Connecting functions to a signal that throw an exception when called is currently undefined behavior.
* All connected functions should handle their own exceptions.
* For backwards-compatibility, the slot function is not required to be noexcept.
*/
KDBINDINGS_WARN_UNUSED ConnectionHandle connectReflective(std::function<void(ConnectionHandle &, Args...)> const &slot)
{
ensureImpl();
return ConnectionHandle{ m_impl, m_impl->connectReflective(slot) };
}
/**
* Establishes a single-shot connection between a signal and a slot and when the signal is emitted, the connection will be
* disconnected and the slot will be called. Note that the slot will be disconnected before it is called. If the slot
* triggers another signal emission of the same signal, the slot will not be called again.
*
* @param slot A std::function that takes the signal's parameter types.
* @return An instance of ConnectionHandle, that can be used to disconnect.
*
* @warning Connecting functions to a signal that throw an exception when called is currently undefined behavior.
* All connected functions should handle their own exceptions.
* For backwards-compatibility, the slot function is not required to be noexcept.
*/
KDBINDINGS_WARN_UNUSED ConnectionHandle connectSingleShot(std::function<void(Args...)> const &slot)
{
return connectReflective([slot](ConnectionHandle &handle, Args... args) {
handle.disconnect();
slot(args...);
});
}
/**
* @brief Establishes a deferred connection between the provided evaluator and slot.
*
* @warning Deferred connections are experimental and may be removed or changed in the future.
*
* This function allows connecting an evaluator and a slot such that the slot's execution
* is deferred until the conditions evaluated by the `evaluator` are met.
*
* First argument to the function is reference to a shared pointer to the ConnectionEvaluator responsible for determining
* when the slot should be executed.
*
* @return An instance of ConnectionHandle, that can be used to disconnect
* or temporarily block the connection.
*
* @note
* The Signal class itself is not thread-safe. While the ConnectionEvaluator is inherently
* thread-safe, ensure that any concurrent access to this Signal is protected externally to maintain thread safety.
*
* @warning Connecting functions to a signal that throw an exception when called is currently undefined behavior.
* All connected functions should handle their own exceptions.
* For backwards-compatibility, the slot function is not required to be noexcept.
*/
KDBINDINGS_WARN_UNUSED ConnectionHandle connectDeferred(const std::shared_ptr<ConnectionEvaluator> &evaluator, std::function<void(Args...)> const &slot)
{
ensureImpl();
ConnectionHandle handle(m_impl, {});
handle.setId(m_impl->connectDeferred(evaluator, slot));
return handle;
}
/**
* A template overload of Signal::connect that makes it easier to connect arbitrary functions to this
* Signal.
* It connects a function to this Signal, binds any provided arguments to that function and discards
* any values emitted by this Signal that aren't needed by the resulting function.
*
* This is especially useful for connecting member functions to signals.
*
* Examples:
* @code
* Signal<int> signal;
* std::vector<int> numbers{ 1, 2, 3 };
* bool emitted = false;
*
* // disambiguation necessary, as push_back is overloaded.
* void (std::vector<int>::*push_back)(const int &) = &std::vector<int>::push_back;
* signal.connect(push_back, &numbers);
*
* // this slot doesn't require the int argument, so it will be discarded.
* signal.connect([&emitted]() { emitted = true; });
*
* signal.emit(4); // Will add 4 to the vector and set emitted to true
* @endcode
*
* For more examples see the @ref 07-advanced-connections/main.cpp example.
*
* @return An instance of a Signal::ConnectionHandle that refers to this connection.
* Warning: When connecting a member function you must use the returned ConnectionHandle
* to disconnect when the object containing the slot goes out of scope!
*
* @warning Connecting functions to a signal that throw an exception when called is currently undefined behavior.
* All connected functions should handle their own exceptions.
* For backwards-compatibility, the slot function is not required to be noexcept.
**/
// The enable_if_t makes sure that this connect function specialization is only
// available if we provide a function that cannot be otherwise converted to a
// std::function<void(Args...)>, as it otherwise tries to take precedence
// over the normal connect function.
template<typename Func, typename... FuncArgs, typename = std::enable_if_t<std::disjunction_v<std::negation<std::is_convertible<Func, std::function<void(Args...)>>>, std::integral_constant<bool, sizeof...(FuncArgs) /*Also enable this function if we want to bind at least one argument*/>>>>
KDBINDINGS_WARN_UNUSED ConnectionHandle connect(Func &&slot, FuncArgs &&...args)
{
std::function<void(Args...)> bound = Private::bind_first(std::forward<Func>(slot), std::forward<FuncArgs>(args)...);
return connect(bound);
}
/**
* Disconnect a previously connected slot.
*
* After the slot was successfully disconnected, the ConnectionHandle will no
* longer be active. (i.e. ConnectionHandle::isActive will return false).
*
* @throw std::out_of_range - If the ConnectionHandle does not belong to this
* Signal (i.e. ConnectionHandle::belongsTo returns false).
*/
void disconnect(const ConnectionHandle &handle)
{
if (m_impl && handle.belongsTo(*this) && handle.m_id.has_value()) {
m_impl->disconnect(handle);
// TODO check if Impl is now empty and reset
} else {
throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
}
}
/**
* Disconnect all previously connected functions.
*
* All currently active ConnectionHandles that belong to this Signal will no
* longer be active afterwards. (i.e. ConnectionHandle::isActive will return false).
*
* @warning While this function is marked with noexcept, it *may* terminate the program
* if it is not possible to allocate memory or if mutex locking isn't possible.
*/
void disconnectAll() noexcept
{
if (m_impl) {
m_impl->disconnectAll();
// Once all connections are disconnected, we can release ownership of the Impl.
// This does not destroy the Signal itself, just the Impl object.
// If another slot is connected, another Impl object will be constructed.
m_impl.reset();
}
// If m_impl is nullptr, we don't have any connections to disconnect
}
/**
* Sets the block state of the connection.
* If a connection is blocked, emitting the Signal will no longer call this
* connections slot, until the connection is unblocked.
*
* ConnectionHandle::block can be used as an alternative.
*
* To temporarily block a connection, consider using an instance of ConnectionBlocker,
* which offers a RAII-style implementation that makes sure the connection is always
* returned to its original state.
*
* @param blocked Whether the connection should be blocked from now on.
* @param handle The ConnectionHandle to block.
* @return Whether the connection was previously blocked.
* @throw std::out_of_range - If the ConnectionHandle does not belong to this
* Signal (i.e. ConnectionHandle::belongsTo returns false).
*/
bool blockConnection(const ConnectionHandle &handle, bool blocked)
{
if (m_impl && handle.belongsTo(*this) && handle.m_id.has_value()) {
return m_impl->blockConnection(*handle.m_id, blocked);
} else {
throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
}
}
/**
* Checks whether the connection is currently blocked.
*
* To change the blocked state of a connection, call blockConnection().
*
* @return Whether the connection is currently blocked
* @throw std::out_of_range - If the ConnectionHandle does not belong to this
* Signal (i.e. ConnectionHandle::belongsTo returns false).
*/
bool isConnectionBlocked(const ConnectionHandle &handle) const
{
assert(handle.belongsTo(*this));
if (!m_impl) {
throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
}
if (handle.m_id.has_value()) {
return m_impl->isConnectionBlocked(*handle.m_id);
} else {
return false;
}
}
/**
* Emits the Signal, which causes all connected slots to be called,
* as long as they are not blocked.
*
* The arguments provided to emit will be passed to each slot by copy,
* therefore consider using (const) references as the Args to the Signal
* wherever possible.
*
* Note: Slots may disconnect themselves during an emit, which will cause the
* connection to be disconnected after all slots have been called.
*
* ⚠️ *Note: Connecting a new slot to a signal while the signal is still
* in the emit function is undefined behavior.*
*
* ⚠️ *Note: This function is **not thread-safe** and **not reentrant**.
* Specifically, this means it is undefined behavior to emit a signal from
* a slot of that same signal.*
*/
void emit(Args... p) const
{
if (m_impl)
m_impl->emit(p...);
// if m_impl is nullptr, we don't have any slots connected, don't bother emitting
}
private:
friend class ConnectionHandle;
void ensureImpl()
{
if (!m_impl) {
m_impl = std::make_shared<Impl>();
}
}
// shared_ptr is used here instead of unique_ptr, so ConnectionHandle instances can
// use a weak_ptr to check if the Signal::Impl they reference is still alive.
//
// This makes Signals easily copyable in theory, but the semantics of this are unclear.
// Copying could either simply copy the shared_ptr, which means the copy would share
// the connections of the original, which is possibly unintuitive, or the Impl would
// have to be copied as well.
// This would however leave connections without handles to disconnect them.
// So copying is forbidden for now.
//
// Think of this shared_ptr more like a unique_ptr with additional weak_ptr's
// in ConnectionHandle that can check whether the Impl object is still alive.
mutable std::shared_ptr<Impl> m_impl;
};
/**
* @brief A ConnectionBlocker is a convenient RAII-style mechanism for temporarily blocking a connection.
*
* When a ConnectionBlocker is constructed, it will block the connection.
*
* When it is destructed, it will return the connection to the blocked state it was in
* before the ConnectionBlocker was constructed.
*
* Example:
* - @ref 08-managing-connections/main.cpp
*/
class ConnectionBlocker
{
public:
/**
* Constructs a new ConnectionBlocker and blocks the connection this ConnectionHandle
* refers to.
*
* @throw std::out_of_range If the connection is not active (i.e. ConnectionHandle::isActive() returns false).
*/
explicit ConnectionBlocker(const ConnectionHandle &handle)
: m_handle{ handle }
{
m_wasBlocked = m_handle.block(true);
}
/**
* Destructs the ConnectionBlocker and returns the connection into the blocked state it was in
* before the ConnectionBlocker was constructed.
*/
~ConnectionBlocker()
{
m_handle.block(m_wasBlocked);
}
private:
ConnectionHandle m_handle;
bool m_wasBlocked{ false };
};
/**
* @example 01-simple-connection/main.cpp
*
* A simple example of how to create a KDBindings::Signal and connect a lambda to it.
*
* The output of this example is:
* ```
* The answer: 42
* ```
*/
/**
* @example 02-signal-member/main.cpp
*
* An example of how to connect a member function to a KDBindings::Signal.
*
* The output of this example is:
* ```
* Hello World!
* ```
*/
/**
* @example 03-member-arguments/main.cpp
*
* An example of how to connect a member function with arguments to a KDBindings::Signal.
*
* The output of this example is:
* ```
* Bob received: Have a nice day!
* Alice received: Thank you!
* ```
*/
/**
* @example 07-advanced-connections/main.cpp
*
* An example of how to use the KDBindings::Signal::connect() overloaded function for advanced slot connections.
*
* The output of this example is:
* ```
* Hello World!
* Emitted value: 5
* true
* ```
*/
/**
* @example 08-managing-connections/main.cpp
*
* An example of how to use a ScopedConnection and ConnectionBlocker to manage
* when a Connection is disconnected or blocked.
*
* Expected output:
* ```
* Guard is connected: 1
* Connection is not blocked: 3
* Connection is not blocked: 5
* ```
*/
} // namespace KDBindings