/* This file is part of KDBindings. SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Sean Harmer SPDX-License-Identifier: MIT Contact KDAB at for commercial licensing options. */ #pragma once #include #include #include #include #include #include #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 #include #include #include /** * @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 Qt's Signals & Slots mechanism 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 class Signal { static_assert( std::conjunction>...>::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 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 &evaluator, std::function const &slot) { auto weakEvaluator = std::weak_ptr(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 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 slot; std::function slotReflective; std::weak_ptr 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 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 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 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 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 &evaluator, std::function 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 signal; * std::vector numbers{ 1, 2, 3 }; * bool emitted = false; * * // disambiguation necessary, as push_back is overloaded. * void (std::vector::*push_back)(const int &) = &std::vector::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, as it otherwise tries to take precedence // over the normal connect function. template>>, std::integral_constant>>> KDBINDINGS_WARN_UNUSED ConnectionHandle connect(Func &&slot, FuncArgs &&...args) { std::function bound = Private::bind_first(std::forward(slot), std::forward(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(); } } // 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 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