diff --git a/src/kdbindings/CMakeLists.txt b/src/kdbindings/CMakeLists.txt index 83c2080..8a99730 100644 --- a/src/kdbindings/CMakeLists.txt +++ b/src/kdbindings/CMakeLists.txt @@ -18,6 +18,8 @@ set(HEADERS property.h property_updater.h signal.h + connection_evaluator.h + connection_handle.h utils.h ) diff --git a/src/kdbindings/connection_evaluator.h b/src/kdbindings/connection_evaluator.h new file mode 100644 index 0000000..c01abf3 --- /dev/null +++ b/src/kdbindings/connection_evaluator.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +#include + +namespace KDBindings { + +/** + * @brief Manages and evaluates deferred Signal connections. + * + * The ConnectionEvaluator class is responsible for managing and evaluating connections + * to Signals. It provides mechanisms to delay and control the evaluation of connections. + * It therefore allows controlling when and on which thread slots connected to a Signal are executed. + * + * @see Signal::connectDeferred() + */ +class ConnectionEvaluator +{ + +public: + ConnectionEvaluator() = default; + + // ConnectionEvaluator is not copyable, as it is designed to manage connections, + // and copying it could lead to unexpected behavior, including duplication of connections and issues + // related to connection lifetimes. Therefore, it is intentionally made non-copyable. + ConnectionEvaluator(const ConnectionEvaluator &) noexcept = delete; + + ConnectionEvaluator &operator=(const ConnectionEvaluator &) noexcept = delete; + + // ConnectionEvaluators are not moveable, as they are captures by-reference + // by the Signal, so moving them would lead to a dangling reference. + ConnectionEvaluator(ConnectionEvaluator &&other) noexcept = delete; + + ConnectionEvaluator &operator=(ConnectionEvaluator &&other) noexcept = delete; + + /** + * @brief Evaluate the deferred connections. + * + * This function is responsible for evaluating and executing deferred connections. + * And this function ensures thread safety + */ + void evaluateDeferredConnections() + { + std::lock_guard lock(m_connectionsMutex); + + for (auto &pair : m_deferredConnections) { + pair.second(); + } + m_deferredConnections.clear(); + } + +private: + template + friend class Signal; + + void enqueueSlotInvocation(const ConnectionHandle &handle, const std::function &connection) + { + std::lock_guard lock(m_connectionsMutex); + m_deferredConnections.push_back({ handle, std::move(connection) }); + } + + void dequeueSlotInvocation(const ConnectionHandle &handle) + { + std::lock_guard lock(m_connectionsMutex); + + auto handleMatches = [&handle](const auto &invocationPair) { + return invocationPair.first == handle; + }; + + // Remove all invocations that match the handle + m_deferredConnections.erase( + std::remove_if(m_deferredConnections.begin(), m_deferredConnections.end(), handleMatches), + m_deferredConnections.end()); + } + void disconnectAllDeferred() + { + // Clear the vector of deferred connections + m_deferredConnections.clear(); + } + + std::vector>> m_deferredConnections; + std::mutex m_connectionsMutex; +}; +} // namespace KDBindings diff --git a/src/kdbindings/connection_handle.h b/src/kdbindings/connection_handle.h new file mode 100644 index 0000000..27abdbd --- /dev/null +++ b/src/kdbindings/connection_handle.h @@ -0,0 +1,213 @@ +/* + This file is part of KDBindings. + + SPDX-FileCopyrightText: 2021-2023 Klarälvdalens Datakonsult AB, a KDAB Group company + Author: Sean Harmer + + SPDX-License-Identifier: MIT + + Contact KDAB at for commercial licensing options. +*/ + +#include +#include + +namespace KDBindings { + +template +class Signal; + +class ConnectionHandle; + +namespace Private { +// +// This class defines a virtual interface, that the Signal this ConnectionHandle refers +// to must implement. +// It allows ConnectionHandle to refer to this non-template class, which then dispatches +// to the template implementation using virtual function calls. +// It allows ConnectionHandle to be a non-template class. +class SignalImplBase : public std::enable_shared_from_this +{ +public: + SignalImplBase() = default; + + virtual ~SignalImplBase() = default; + + virtual void disconnect(const ConnectionHandle &handle) = 0; + virtual bool blockConnection(const GenerationalIndex &id, bool blocked) = 0; + virtual bool isConnectionActive(const GenerationalIndex &id) const = 0; + virtual bool isConnectionBlocked(const GenerationalIndex &id) const = 0; +}; + +} // namespace Private + +/** + * @brief A ConnectionHandle represents the connection of a Signal + * to a slot (i.e. a function that is called when the Signal is emitted). + * + * It is returned from a Signal when a connection is created and used to + * manage the connection by disconnecting, (un)blocking it and checking its state. + **/ +class ConnectionHandle +{ +public: + /** + * A ConnectionHandle can be default constructed. + * In this case the ConnectionHandle will not reference any active connection (i.e. isActive() will return false), + * and not belong to any Signal. + **/ + ConnectionHandle() = default; + + /** + * A ConnectionHandle can be copied. + **/ + ConnectionHandle(const ConnectionHandle &) = default; + ConnectionHandle &operator=(const ConnectionHandle &) = default; + + /** + * A ConnectionHandle can be moved. + **/ + ConnectionHandle(ConnectionHandle &&) = default; + ConnectionHandle &operator=(ConnectionHandle &&) = default; + + /** + * Disconnect the slot. + * + * When this function is called, the function that was passed to Signal::connect + * to create this ConnectionHandle will no longer be called when the Signal is emitted. + * + * If the ConnectionHandle is not active or the connection has already been disconnected, + * nothing happens. + * + * After this call, the ConnectionHandle will be inactive (i.e. isActive() returns false) + * and will no longer belong to any Signal (i.e. belongsTo returns false). + **/ + void disconnect() + { + if (m_id.has_value()) { + if (auto shared_impl = checkedLock()) { + shared_impl->disconnect(*this); + } + } + + // ConnectionHandle is no longer active; + m_signalImpl.reset(); + } + + /** + * Check whether the connection of this ConnectionHandle is active. + * + * @return true if the ConnectionHandle refers to an active Signal + * and the connection was not disconnected previously, false otherwise. + **/ + bool isActive() const + { + return static_cast(checkedLock()); + } + + /** + * 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. + * + * Behaves the same as calling Signal::blockConnection with this + * ConnectionHandle as argument. + * + * 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 The new blocked state of the connection. + * @return whether the connection was previously blocked. + * @throw std::out_of_range Throws if the connection is not active (i.e. isActive() returns false). + **/ + bool block(bool blocked) + { + if (m_id.has_value()) { + if (auto shared_impl = checkedLock()) { + return shared_impl->blockConnection(*m_id, blocked); + } + } + throw std::out_of_range("Cannot block a non-active connection!"); + } + + /** + * Checks whether the connection is currently blocked. + * + * To change the blocked state of a connection, call ConnectionHandle::block. + * + * @return whether the connection is currently blocked. + **/ + bool isBlocked() const + { + if (m_id.has_value()) + if (auto shared_impl = checkedLock()) { + return shared_impl->isConnectionBlocked(*m_id); + } + throw std::out_of_range("Cannot check whether a non-active connection is blocked!"); + } + + /** + * Check whether this ConnectionHandle belongs to the given Signal. + * + * @return true if this ConnectionHandle refers to a connection within the given Signal + **/ + template + bool belongsTo(const Signal &signal) const + { + auto shared_impl = m_signalImpl.lock(); + return shared_impl && shared_impl == std::static_pointer_cast(signal.m_impl); + } + + // Define an operator== function to compare ConnectionHandle objects. + bool operator==(const ConnectionHandle &other) const + { + auto thisSignalImpl = m_signalImpl.lock(); + auto otherSignalImpl = other.m_signalImpl.lock(); + + // If both signalImpl pointers are valid, compare them along with the IDs. + if (thisSignalImpl && otherSignalImpl) { + return (thisSignalImpl == otherSignalImpl) && (m_id == other.m_id); + } + + // If neither instance has an ID, and both signalImpl pointers are invalid, consider them equal. + if (!m_id.has_value() && !other.m_id.has_value() && !thisSignalImpl && !otherSignalImpl) { + return true; + } + + // In all other cases, they are not equal. + return false; + } + +private: + template + friend class Signal; + + std::weak_ptr m_signalImpl; + std::optional m_id; + + // private, so it is only available from Signal + ConnectionHandle(std::weak_ptr signalImpl, std::optional id) + : m_signalImpl{ std::move(signalImpl) }, m_id{ std::move(id) } + { + } + void setId(const Private::GenerationalIndex &id) + { + m_id = id; + } + + // Checks that the weak_ptr can be locked and that the connection is + // still active + std::shared_ptr checkedLock() const + { + auto shared_impl = m_signalImpl.lock(); + if (m_id.has_value()) { + if (shared_impl && shared_impl->isConnectionActive(*m_id)) { + return shared_impl; + } + } + return nullptr; + } +}; + +} // namespace KDBindings \ No newline at end of file diff --git a/src/kdbindings/genindex_array.h b/src/kdbindings/genindex_array.h index ab5cddf..9205d11 100644 --- a/src/kdbindings/genindex_array.h +++ b/src/kdbindings/genindex_array.h @@ -36,6 +36,11 @@ namespace Private { struct GenerationalIndex { uint32_t index = 0; uint32_t generation = 0; + + bool operator==(const GenerationalIndex& rhs) const { + // Include both index and generation in the equality check + return index == rhs.index && generation == rhs.generation; + } }; class GenerationalIndexAllocator diff --git a/src/kdbindings/signal.h b/src/kdbindings/signal.h index df21d25..00194ce 100644 --- a/src/kdbindings/signal.h +++ b/src/kdbindings/signal.h @@ -12,13 +12,12 @@ #pragma once #include -#include #include -#include #include #include #include +#include #include #include @@ -29,168 +28,6 @@ */ namespace KDBindings { -template -class Signal; - -namespace Private { -// -// This class defines a virtual interface, that the Signal this ConnectionHandle refers -// to must implement. -// It allows ConnectionHandle to refer to this non-template class, which then dispatches -// to the template implementation using virtual function calls. -// It allows ConnectionHandle to be a non-template class. -class SignalImplBase -{ -public: - SignalImplBase() = default; - - virtual ~SignalImplBase() = default; - - virtual void disconnect(const GenerationalIndex &id) = 0; - virtual bool blockConnection(const GenerationalIndex &id, bool blocked) = 0; - virtual bool isConnectionActive(const GenerationalIndex &id) const = 0; - virtual bool isConnectionBlocked(const GenerationalIndex &id) const = 0; -}; - -} // namespace Private - -/** - * @brief A ConnectionHandle represents the connection of a Signal - * to a slot (i.e. a function that is called when the Signal is emitted). - * - * It is returned from a Signal when a connection is created and used to - * manage the connection by disconnecting, (un)blocking it and checking its state. - **/ -class ConnectionHandle -{ -public: - /** - * A ConnectionHandle can be default constructed. - * In this case the ConnectionHandle will not reference any active connection (i.e. isActive() will return false), - * and not belong to any Signal. - **/ - ConnectionHandle() = default; - - /** - * A ConnectionHandle can be copied. - **/ - ConnectionHandle(const ConnectionHandle &) = default; - ConnectionHandle &operator=(const ConnectionHandle &) = default; - - /** - * A ConnectionHandle can be moved. - **/ - ConnectionHandle(ConnectionHandle &&) = default; - ConnectionHandle &operator=(ConnectionHandle &&) = default; - - /** - * Disconnect the slot. - * - * When this function is called, the function that was passed to Signal::connect - * to create this ConnectionHandle will no longer be called when the Signal is emitted. - * - * If the ConnectionHandle is not active or the connection has already been disconnected, - * nothing happens. - * - * After this call, the ConnectionHandle will be inactive (i.e. isActive() returns false) - * and will no longer belong to any Signal (i.e. belongsTo returns false). - **/ - void disconnect() - { - if (auto shared_impl = checkedLock()) { - shared_impl->disconnect(m_id); - } - // ConnectionHandle is no longer active; - m_signalImpl.reset(); - } - - /** - * Check whether the connection of this ConnectionHandle is active. - * - * @return true if the ConnectionHandle refers to an active Signal - * and the connection was not disconnected previously, false otherwise. - **/ - bool isActive() const - { - return static_cast(checkedLock()); - } - - /** - * 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. - * - * Behaves the same as calling Signal::blockConnection with this - * ConnectionHandle as argument. - * - * 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 The new blocked state of the connection. - * @return whether the connection was previously blocked. - * @throw std::out_of_range Throws if the connection is not active (i.e. isActive() returns false). - **/ - bool block(bool blocked) - { - if (auto shared_impl = checkedLock()) { - return shared_impl->blockConnection(m_id, blocked); - } - throw std::out_of_range("Cannot block a non-active connection!"); - } - - /** - * Checks whether the connection is currently blocked. - * - * To change the blocked state of a connection, call ConnectionHandle::block. - * - * @return whether the connection is currently blocked. - **/ - bool isBlocked() const - { - if (auto shared_impl = checkedLock()) { - return shared_impl->isConnectionBlocked(m_id); - } - throw std::out_of_range("Cannot check whether a non-active connection is blocked!"); - } - - /** - * Check whether this ConnectionHandle belongs to the given Signal. - * - * @return true if this ConnectionHandle refers to a connection within the given Signal - **/ - template - bool belongsTo(const Signal &signal) const - { - auto shared_impl = m_signalImpl.lock(); - return shared_impl && shared_impl == std::static_pointer_cast(signal.m_impl); - } - -private: - template - friend class Signal; - - std::weak_ptr m_signalImpl; - Private::GenerationalIndex m_id; - - // private, so it is only available from Signal - ConnectionHandle(std::weak_ptr signalImpl, Private::GenerationalIndex id) - : m_signalImpl{ std::move(signalImpl) }, m_id{ std::move(id) } - { - } - - // Checks that the weak_ptr can be locked and that the connection is - // still active - std::shared_ptr checkedLock() const - { - auto shared_impl = m_signalImpl.lock(); - if (shared_impl && shared_impl->isConnectionActive(m_id)) { - return shared_impl; - } - return nullptr; - } -}; - /** * @brief A Signal provides a mechanism for communication between objects. * @@ -204,6 +41,14 @@ class ConnectionHandle * * 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 @@ -239,13 +84,57 @@ class Signal // value can be used to disconnect the function again. Private::GenerationalIndex connect(std::function const &slot) { - return m_connections.insert({ 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](const ConnectionHandle &handle, Args... args) { + if (auto evaluatorPtr = weakEvaluator.lock()) { + auto lambda = [slot, handle, args...]() { + slot(handle, args...); // Call slot with ConnectionHandle and Args + }; + evaluatorPtr->enqueueSlotInvocation(handle, lambda); + } else { + throw std::runtime_error("ConnectionEvaluator is no longer alive"); + } + }; + + Connection newConnection; + newConnection.m_connectionEvaluator = evaluator; + newConnection.slotDeferred = deferredSlot; + + return m_connections.insert(std::move(newConnection)); } // Disconnects a previously connected function - void disconnect(const Private::GenerationalIndex &id) override + void disconnect(const ConnectionHandle &handle) override { - m_connections.erase(id); + // 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 && connection->slotDeferred) { + if (auto evaluatorPtr = connection->m_connectionEvaluator.lock()) { + evaluatorPtr->dequeueSlotInvocation(handle); + } + } + + m_connections.erase(id); + } } // Disconnects all previously connected functions @@ -254,6 +143,29 @@ class Signal m_connections.clear(); } + void disconnectAllConnections() + { + const auto numEntries = m_connections.entriesSize(); + + for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) { + const auto indexOpt = m_connections.indexAtEntry(i); + if (indexOpt) { + const auto con = m_connections.get(*indexOpt); + if (con) { + if (con->slot) { + m_connections.clear(); + } + + if (con->slotDeferred && con->m_connectionEvaluator.lock()) { + con->m_connectionEvaluator.lock()->disconnectAllDeferred(); + } + + m_connections.erase(*indexOpt); + } + } + } + } + bool blockConnection(const Private::GenerationalIndex &id, bool blocked) override { Connection *connection = m_connections.get(id); @@ -281,9 +193,9 @@ class Signal } } - // Calls all connected functions - void emit(Args... p) const + void emit(Args... p) { + const auto numEntries = m_connections.entriesSize(); // This loop can tolerate signal handles being disconnected inside a slot, @@ -294,17 +206,30 @@ class Signal if (index) { const auto con = m_connections.get(*index); - if (!con->blocked) - con->slot(p...); + if (!con->blocked) { + if (con->slotDeferred) { + // Check if shared_from_this is safe to call + if (auto sharedThis = shared_from_this(); sharedThis) { + ConnectionHandle handle(sharedThis, *index); + con->slotDeferred(handle, p...); + } + } else if (con->slot) { + con->slot(p...); + } + } } } } private: + friend class Signal; struct Connection { std::function slot; + std::function slotDeferred; + std::weak_ptr m_connectionEvaluator; bool blocked{ false }; }; + mutable Private::GenerationalIndexArray m_connections; }; @@ -330,7 +255,11 @@ class Signal */ ~Signal() { - disconnectAll(); + if (m_impl) { + m_impl->disconnectAllConnections(); + } else { + disconnectAll(); + } } /** @@ -349,6 +278,37 @@ class Signal return ConnectionHandle{ m_impl, m_impl->connect(slot) }; } + /** + * @brief Establishes a deferred connection between the provided evaluator and slot. + * + * 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 `KDBindings::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. + */ + ConnectionHandle connectDeferred(const std::shared_ptr &evaluator, std::function const &slot) + { + ensureImpl(); + + // Create a wrapper function that includes the ConnectionHandle + auto wrapper = [slot](const ConnectionHandle &handle, Args... args) { + slot(args...); + }; + + // Call connectDeferred on the implementation with the wrapper function + ConnectionHandle handle(m_impl, {}); + handle.setId(m_impl->connectDeferred(evaluator, wrapper)); + return handle; + } + /** * A template overload of Signal::connect that makes it easier to connect arbitrary functions to this * Signal. @@ -401,8 +361,8 @@ class Signal */ void disconnect(const ConnectionHandle &handle) { - if (m_impl && handle.belongsTo(*this)) { - m_impl->disconnect(handle.m_id); + 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!"); @@ -446,8 +406,8 @@ class Signal */ bool blockConnection(const ConnectionHandle &handle, bool blocked) { - if (m_impl && handle.belongsTo(*this)) { - return m_impl->blockConnection(handle.m_id, 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!"); } @@ -469,7 +429,11 @@ class Signal throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!"); } - return m_impl->isConnectionBlocked(handle.m_id); + if (handle.m_id.has_value()) { + return m_impl->isConnectionBlocked(*handle.m_id); + } else { + return false; + } } /** diff --git a/tests/signal/tst_signal.cpp b/tests/signal/tst_signal.cpp index 52066d0..c6bee62 100644 --- a/tests/signal/tst_signal.cpp +++ b/tests/signal/tst_signal.cpp @@ -11,9 +11,11 @@ #include "kdbindings/utils.h" #include +#include #include #include +#include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include @@ -77,6 +79,162 @@ TEST_CASE("Signal connections") REQUIRE(lambdaCalled == true); } + SUBCASE("Disconnect Deferred Connection") + { + Signal signal1; + Signal signal2; + int val = 4; + auto evaluator = std::make_shared(); + + auto connection1 = signal1.connectDeferred(evaluator, [&val](int value) { + val += value; + }); + + auto connection2 = signal2.connectDeferred(evaluator, [&val](int value1, int value2) { + val += value1; + val += value2; + }); + + REQUIRE(connection1.isActive()); + + signal1.emit(4); + REQUIRE(val == 4); // val not changing immediately after emit + + signal2.emit(3, 2); + REQUIRE(val == 4); // val not changing immediately after emit + + connection1.disconnect(); + REQUIRE(!connection1.isActive()); + + REQUIRE(connection2.isActive()); + + evaluator->evaluateDeferredConnections(); // It doesn't evaluate any slots of signal1 as it ConnectionHandle gets disconnected before the evaluation of the deferred connections. + REQUIRE(val == 9); + } + + SUBCASE("Multiple Signals with Evaluator") + { + Signal signal1; + Signal signal2; + int val = 4; + auto evaluator = std::make_shared(); + + std::thread thread1([&] { + signal1.connectDeferred(evaluator, [&val](int value) { + val += value; + }); + }); + + std::thread thread2([&] { + signal2.connectDeferred(evaluator, [&val](int value) { + val += value; + }); + }); + + thread1.join(); + thread2.join(); + + signal1.emit(2); + signal2.emit(3); + REQUIRE(val == 4); // val not changing immediately after emit + + evaluator->evaluateDeferredConnections(); + + REQUIRE(val == 9); + } + + SUBCASE("Emit Multiple Signals with Evaluator") + { + Signal signal1; + Signal signal2; + int val1 = 4; + int val2 = 4; + auto evaluator = std::make_shared(); + + signal1.connectDeferred(evaluator, [&val1](int value) { + val1 += value; + }); + + signal2.connectDeferred(evaluator, [&val2](int value) { + val2 += value; + }); + + std::thread thread1([&] { + signal1.emit(2); + }); + + std::thread thread2([&] { + signal2.emit(3); + }); + + thread1.join(); + thread2.join(); + + REQUIRE(val1 == 4); + REQUIRE(val2 == 4); + + evaluator->evaluateDeferredConnections(); + + REQUIRE(val1 == 6); + REQUIRE(val2 == 7); + } + + SUBCASE("Deferred Connect, Emit, Disconnect, and Evaluate") + { + Signal signal; + int val = 4; + auto evaluator = std::make_shared(); + + auto connection = signal.connectDeferred(evaluator, [&val](int value) { + val += value; + }); + + REQUIRE(connection.isActive()); + + signal.emit(2); + REQUIRE(val == 4); + + connection.disconnect(); + evaluator->evaluateDeferredConnections(); // It doesn't evaluate the slot as the signal gets disconnected before it's evaluation of the deferred connections. + + REQUIRE(val == 4); + } + + SUBCASE("Double Evaluate Deferred Connections") + { + Signal signal; + int val = 4; + auto evaluator = std::make_shared(); + + signal.connectDeferred(evaluator, [&val](int value) { + val += value; + }); + + signal.emit(2); + REQUIRE(val == 4); + + evaluator->evaluateDeferredConnections(); + evaluator->evaluateDeferredConnections(); + + REQUIRE(val == 6); + } + + SUBCASE("Disconnect deferred connection from deleted signal") + { + auto signal = new Signal<>(); + auto evaluator = std::make_shared(); + bool called = false; + + auto handle = signal->connectDeferred(evaluator, [&called]() { called = true; }); + signal->emit(); + delete signal; + + handle.disconnect(); + REQUIRE(called == false); + evaluator->evaluateDeferredConnections(); + REQUIRE(called == false); + } + SUBCASE("A signal with arguments can be connected to a lambda and invoked with l-value args") { Signal signal;