/* This file is part of KDBindings. SPDX-FileCopyrightText: 2021-2022 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 #include #include #include /** * @brief The main namespace of the KDBindings library. * * All public parts of KDBindings are members of this namespace. */ 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. * * To make sure a Connection to an object is disconnected correctly, consider * storing a ScopedConnection to its ConnectionHandle inside the object. **/ 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. * * 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. * * 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) { return m_connections.insert({ slot }); } // Disconnects a previously connected function void disconnect(const Private::GenerationalIndex &id) override { m_connections.erase(id); } // Disconnects all previously connected functions void disconnectAll() { m_connections.clear(); } 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 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!"); } } // Calls all connected functions void emit(Args... p) const { const auto numEntries = m_connections.entriesSize(); // This loop can tolerate signal handles being disconnected inside a slot, // but adding new connections to a signal inside a slot will still be undefined behaviour 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) con->slot(p...); } } } private: struct Connection { std::function slot; bool blocked{ false }; }; mutable Private::GenerationalIndexArray m_connections; }; 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). */ ~Signal() { 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. */ ConnectionHandle connect(std::function const &slot) { ensureImpl(); return ConnectionHandle{ m_impl, m_impl->connect(slot) }; } /** * 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! **/ // 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>>> 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)) { m_impl->disconnect(handle.m_id); // 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). */ void disconnectAll() { 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)) { 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!"); } return m_impl->isConnectionBlocked(handle.m_id); } /** * 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, however it is * undefined whether a slot that is connected during the emit function * of the Signal will also be called during this emit, or only at the next * emit. */ 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 ScopedConnection is a RAII-style way to make sure a Connection is disconnected. * * When the ScopedConnections scope ends, the connection this ScopedConnection guards will be disconnected. * * Example: * - @ref 08-managing-connections/main.cpp */ class ScopedConnection { public: /** * @brief A ScopedConnection can be default constructed * * A default constructed ScopedConnection has no connection to guard. * Therefore it does nothing when it is destructed, unless a ConnectionHandle is assigned to it. */ ScopedConnection() = default; /** A ScopedConnection can be move constructed */ ScopedConnection(ScopedConnection &&) = default; /** A ScopedConnection cannot be copied */ ScopedConnection(const ScopedConnection &) = delete; /** A ScopedConnection cannot be copied */ ScopedConnection &operator=(const ScopedConnection &) = delete; /** A ScopedConnection can be move assigned */ ScopedConnection &operator=(ScopedConnection &&other) { m_connection.disconnect(); m_connection = std::move(other.m_connection); return *this; } /** * A ScopedConnection can be constructed from a ConnectionHandle */ ScopedConnection(ConnectionHandle &&h) : m_connection(std::move(h)) { } /** * A ScopedConnection can be assigned from a ConnectionHandle */ ScopedConnection &operator=(ConnectionHandle &&h) { return *this = ScopedConnection(std::move(h)); } /** * @return the handle to the connection this instance is managing */ ConnectionHandle &handle() { return m_connection; } /** * @overload */ const ConnectionHandle &handle() const { return m_connection; } /** * Convenience access to the underlying ConnectionHandle using the `->` operator. */ ConnectionHandle *operator->() { return &m_connection; } /** * @overload */ const ConnectionHandle *operator->() const { return &m_connection; } /** * When a ConnectionHandle is destructed it disconnects the connection it guards. */ ~ScopedConnection() { m_connection.disconnect(); } private: ConnectionHandle m_connection; }; /** * @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