aboutsummaryrefslogtreecommitdiff
path: root/include/nod.hpp
diff options
context:
space:
mode:
authorSamuel Fadel <samuel@nihil.ws>2023-05-23 11:22:33 +0200
committerSamuel Fadel <samuel@nihil.ws>2023-05-23 11:22:33 +0200
commit0f34fd437efb936ef29ac91186321aa7251fbfb1 (patch)
tree271e994828f4bb19c35b2630f2705cb64b8d4552 /include/nod.hpp
parentbedf6936885694688ddb8bd3452f6bd68ef8d29c (diff)
Massive changes in initial port away from Qt.
Diffstat (limited to 'include/nod.hpp')
-rw-r--r--include/nod.hpp684
1 files changed, 684 insertions, 0 deletions
diff --git a/include/nod.hpp b/include/nod.hpp
new file mode 100644
index 0000000..2403244
--- /dev/null
+++ b/include/nod.hpp
@@ -0,0 +1,684 @@
+#ifndef IG_NOD_INCLUDE_NOD_HPP
+#define IG_NOD_INCLUDE_NOD_HPP
+
+#include <vector> // std::vector
+#include <functional> // std::function
+#include <mutex> // std::mutex, std::lock_guard
+#include <memory> // std::shared_ptr, std::weak_ptr
+#include <algorithm> // std::find_if()
+#include <cassert> // assert()
+#include <thread> // std::this_thread::yield()
+#include <type_traits> // std::is_same
+#include <iterator> // std::back_inserter
+
+namespace nod {
+ // implementational details
+ namespace detail {
+ /// Interface for type erasure when disconnecting slots
+ struct disconnector {
+ virtual void operator()( std::size_t index ) const = 0;
+ };
+ /// Deleter that doesn't delete
+ inline void no_delete(disconnector*){
+ }
+ } // namespace detail
+
+ /// Base template for the signal class
+ template <class P, class T>
+ class signal_type;
+
+
+ /// Connection class.
+ ///
+ /// This is used to be able to disconnect slots after they have been connected.
+ /// Used as return type for the connect method of the signals.
+ ///
+ /// Connections are default constructible.
+ /// Connections are not copy constructible or copy assignable.
+ /// Connections are move constructible and move assignable.
+ ///
+ class connection {
+ public:
+ /// Default constructor
+ connection() :
+ _index()
+ {}
+
+ // Connection are not copy constructible or copy assignable
+ connection( connection const& ) = delete;
+ connection& operator=( connection const& ) = delete;
+
+ /// Move constructor
+ /// @param other The instance to move from.
+ connection( connection&& other ) :
+ _weak_disconnector( std::move(other._weak_disconnector) ),
+ _index( other._index )
+ {}
+
+ /// Move assign operator.
+ /// @param other The instance to move from.
+ connection& operator=( connection&& other ) {
+ _weak_disconnector = std::move( other._weak_disconnector );
+ _index = other._index;
+ return *this;
+ }
+
+ /// @returns `true` if the connection is connected to a signal object,
+ /// and `false` otherwise.
+ bool connected() const {
+ return !_weak_disconnector.expired();
+ }
+
+ /// Disconnect the slot from the connection.
+ ///
+ /// If the connection represents a slot that is connected to a signal object, calling
+ /// this method will disconnect the slot from that object. The result of this operation
+ /// is that the slot will stop receiving calls when the signal is invoked.
+ void disconnect();
+
+ private:
+ /// The signal template is a friend of the connection, since it is the
+ /// only one allowed to create instances using the meaningful constructor.
+ template<class P,class T> friend class signal_type;
+
+ /// Create a connection.
+ /// @param shared_disconnector Disconnector instance that will be used to disconnect
+ /// the connection when the time comes. A weak pointer
+ /// to the disconnector will be held within the connection
+ /// object.
+ /// @param index The slot index of the connection.
+ connection( std::shared_ptr<detail::disconnector> const& shared_disconnector, std::size_t index ) :
+ _weak_disconnector( shared_disconnector ),
+ _index( index )
+ {}
+
+ /// Weak pointer to the current disconnector functor.
+ std::weak_ptr<detail::disconnector> _weak_disconnector;
+ /// Slot index of the connected slot.
+ std::size_t _index;
+ };
+
+ /// Scoped connection class.
+ ///
+ /// This type of connection is automatically disconnected when
+ /// the connection object is destructed.
+ ///
+ class scoped_connection
+ {
+ public:
+ /// Scoped are default constructible
+ scoped_connection() = default;
+ /// Scoped connections are not copy constructible
+ scoped_connection( scoped_connection const& ) = delete;
+ /// Scoped connections are not copy assingable
+ scoped_connection& operator=( scoped_connection const& ) = delete;
+
+ /// Move constructor
+ scoped_connection( scoped_connection&& other ) :
+ _connection( std::move(other._connection) )
+ {}
+
+ /// Move assign operator.
+ /// @param other The instance to move from.
+ scoped_connection& operator=( scoped_connection&& other ) {
+ reset( std::move( other._connection ) );
+ return *this;
+ }
+
+ /// Construct a scoped connection from a connection object
+ /// @param connection The connection object to manage
+ scoped_connection( connection&& c ) :
+ _connection( std::forward<connection>(c) )
+ {}
+
+ /// destructor
+ ~scoped_connection() {
+ disconnect();
+ }
+
+ /// Assignment operator moving a new connection into the instance.
+ /// @note If the scoped_connection instance already contains a
+ /// connection, that connection will be disconnected as if
+ /// the scoped_connection was destroyed.
+ /// @param c New connection to manage
+ scoped_connection& operator=( connection&& c ) {
+ reset( std::forward<connection>(c) );
+ return *this;
+ }
+
+ /// Reset the underlying connection to another connection.
+ /// @note The connection currently managed by the scoped_connection
+ /// instance will be disconnected when resetting.
+ /// @param c New connection to manage
+ void reset( connection&& c = {} ) {
+ disconnect();
+ _connection = std::move(c);
+ }
+
+ /// Release the underlying connection, without disconnecting it.
+ /// @returns The newly released connection instance is returned.
+ connection release() {
+ connection c = std::move(_connection);
+ _connection = connection{};
+ return c;
+ }
+
+ ///
+ /// @returns `true` if the connection is connected to a signal object,
+ /// and `false` otherwise.
+ bool connected() const {
+ return _connection.connected();
+ }
+
+ /// Disconnect the slot from the connection.
+ ///
+ /// If the connection represents a slot that is connected to a signal object, calling
+ /// this method will disconnect the slot from that object. The result of this operation
+ /// is that the slot will stop receiving calls when the signal is invoked.
+ void disconnect() {
+ _connection.disconnect();
+ }
+
+ private:
+ /// Underlying connection object
+ connection _connection;
+ };
+
+ /// Policy for multi threaded use of signals.
+ ///
+ /// This policy provides mutex and lock types for use in
+ /// a multithreaded environment, where signals and slots
+ /// may exists in different threads.
+ ///
+ /// This policy is used in the `nod::signal` type provided
+ /// by the library.
+ struct multithread_policy
+ {
+ using mutex_type = std::mutex;
+ using mutex_lock_type = std::unique_lock<mutex_type>;
+ /// Function that yields the current thread, allowing
+ /// the OS to reschedule.
+ static void yield_thread() {
+ std::this_thread::yield();
+ }
+ /// Function that defers a lock to a lock function that prevents deadlock
+ static mutex_lock_type defer_lock(mutex_type & m){
+ return mutex_lock_type{m, std::defer_lock};
+ }
+ /// Function that locks two mutexes and prevents deadlock
+ static void lock(mutex_lock_type & a,mutex_lock_type & b) {
+ std::lock(a,b);
+ }
+ };
+
+ /// Policy for single threaded use of signals.
+ ///
+ /// This policy provides dummy implementations for mutex
+ /// and lock types, resulting in that no synchronization
+ /// will take place.
+ ///
+ /// This policy is used in the `nod::unsafe_signal` type
+ /// provided by the library.
+ struct singlethread_policy
+ {
+ /// Dummy mutex type that doesn't do anything
+ struct mutex_type{};
+ /// Dummy lock type, that doesn't do any locking.
+ struct mutex_lock_type
+ {
+ /// A lock type must be constructible from a
+ /// mutex type from the same thread policy.
+ explicit mutex_lock_type( mutex_type const& ) {
+ }
+ };
+ /// Dummy implementation of thread yielding, that
+ /// doesn't do any actual yielding.
+ static void yield_thread() {
+ }
+ /// Dummy implemention of defer_lock that doesn't
+ /// do anything
+ static mutex_lock_type defer_lock(mutex_type &m){
+ return mutex_lock_type{m};
+ }
+ /// Dummy implemention of lock that doesn't
+ /// do anything
+ static void lock(mutex_lock_type &,mutex_lock_type &) {
+ }
+ };
+
+ /// Signal accumulator class template.
+ ///
+ /// This acts sort of as a proxy for triggering a signal and
+ /// accumulating the slot return values.
+ ///
+ /// This class is not really intended to instantiate by client code.
+ /// Instances are aquired as return values of the method `accumulate()`
+ /// called on signals.
+ ///
+ /// @tparam S Type of signal. The signal_accumulator acts
+ /// as a type of proxy for a signal instance of
+ /// this type.
+ /// @tparam T Type of initial value of the accumulate algorithm.
+ /// This type must meet the requirements of `CopyAssignable`
+ /// and `CopyConstructible`
+ /// @tparam F Type of accumulation function.
+ /// @tparam A... Argument types of the underlying signal type.
+ ///
+ template <class S, class T, class F, class...A>
+ class signal_accumulator
+ {
+ public:
+ /// Result type when calling the accumulating function operator.
+ #if (__cplusplus > 201703L)
+ using result_type = typename std::invoke_result<F, T, typename S::slot_type::result_type>::type;
+ #else
+ using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::type;
+ #endif
+
+ /// Construct a signal_accumulator as a proxy to a given signal
+ //
+ /// @param signal Signal instance.
+ /// @param init Initial value of the accumulate algorithm.
+ /// @param func Binary operation function object that will be
+ /// applied to all slot return values.
+ /// The signature of the function should be
+ /// equivalent of the following:
+ /// `R func( T1 const& a, T2 const& b )`
+ /// - The signature does not need to have `const&`.
+ /// - The initial value, type `T`, must be implicitly
+ /// convertible to `R`
+ /// - The return type `R` must be implicitly convertible
+ /// to type `T1`.
+ /// - The type `R` must be `CopyAssignable`.
+ /// - The type `S::slot_type::result_type` (return type of
+ /// the signals slots) must be implicitly convertible to
+ /// type `T2`.
+ signal_accumulator( S const& signal, T init, F func ) :
+ _signal( signal ),
+ _init( init ),
+ _func( func )
+ {}
+
+ /// Function call operator.
+ ///
+ /// Calling this will trigger the underlying signal and accumulate
+ /// all of the connected slots return values with the current
+ /// initial value and accumulator function.
+ ///
+ /// When called, this will invoke the accumulator function will
+ /// be called for each return value of the slots. The semantics
+ /// are similar to the `std::accumulate` algorithm.
+ ///
+ /// @param args Arguments to propagate to the slots of the
+ /// underlying when triggering the signal.
+ result_type operator()( A const& ... args ) const {
+ return _signal.trigger_with_accumulator( _init, _func, args... );
+ }
+
+ private:
+
+ /// Reference to the underlying signal to proxy.
+ S const& _signal;
+ /// Initial value of the accumulate algorithm.
+ T _init;
+ /// Accumulator function.
+ F _func;
+
+ };
+
+ /// Signal template specialization.
+ ///
+ /// This is the main signal implementation, and it is used to
+ /// implement the observer pattern whithout the overhead
+ /// boilerplate code that typically comes with it.
+ ///
+ /// Any function or function object is considered a slot, and
+ /// can be connected to a signal instance, as long as the signature
+ /// of the slot matches the signature of the signal.
+ ///
+ /// @tparam P Threading policy for the signal.
+ /// A threading policy must provide two type definitions:
+ /// - P::mutex_type, this type will be used as a mutex
+ /// in the signal_type class template.
+ /// - P::mutex_lock_type, this type must implement a
+ /// constructor that takes a P::mutex_type as a parameter,
+ /// and it must have the semantics of a scoped mutex lock
+ /// like std::lock_guard, i.e. locking in the constructor
+ /// and unlocking in the destructor.
+ ///
+ /// @tparam R Return value type of the slots connected to the signal.
+ /// @tparam A... Argument types of the slots connected to the signal.
+ template <class P, class R, class... A >
+ class signal_type<P,R(A...)>
+ {
+ public:
+ /// signals are not copy constructible
+ signal_type( signal_type const& ) = delete;
+ /// signals are not copy assignable
+ signal_type& operator=( signal_type const& ) = delete;
+ /// signals are move constructible
+ signal_type(signal_type&& other)
+ {
+ mutex_lock_type lock{other._mutex};
+ _slot_count = std::move(other._slot_count);
+ _slots = std::move(other._slots);
+ if(other._shared_disconnector != nullptr)
+ {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::move(other._shared_disconnector);
+ // replace the disconnector with our own disconnector
+ *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
+ }
+ }
+ /// signals are move assignable
+ signal_type& operator=(signal_type&& other)
+ {
+ auto lock = thread_policy::defer_lock(_mutex);
+ auto other_lock = thread_policy::defer_lock(other._mutex);
+ thread_policy::lock(lock,other_lock);
+
+ _slot_count = std::move(other._slot_count);
+ _slots = std::move(other._slots);
+ if(other._shared_disconnector != nullptr)
+ {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::move(other._shared_disconnector);
+ // replace the disconnector with our own disconnector
+ *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
+ }
+ return *this;
+ }
+
+ /// signals are default constructible
+ signal_type() :
+ _slot_count(0)
+ {}
+
+ // Destruct the signal object.
+ ~signal_type() {
+ invalidate_disconnector();
+ }
+
+ /// Type that will be used to store the slots for this signal type.
+ using slot_type = std::function<R(A...)>;
+ /// Type that is used for counting the slots connected to this signal.
+ using size_type = typename std::vector<slot_type>::size_type;
+
+
+ /// Connect a new slot to the signal.
+ ///
+ /// The connected slot will be called every time the signal
+ /// is triggered.
+ /// @param slot The slot to connect. This must be a callable with
+ /// the same signature as the signal itself.
+ /// @return A connection object is returned, and can be used to
+ /// disconnect the slot.
+ template <class T>
+ connection connect( T&& slot ) {
+ mutex_lock_type lock{ _mutex };
+ _slots.push_back( std::forward<T>(slot) );
+ std::size_t index = _slots.size()-1;
+ if( _shared_disconnector == nullptr ) {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::shared_ptr<detail::disconnector>{&_disconnector, detail::no_delete};
+ }
+ ++_slot_count;
+ return connection{ _shared_disconnector, index };
+ }
+
+ /// Function call operator.
+ ///
+ /// Calling this is how the signal is triggered and the
+ /// connected slots are called.
+ ///
+ /// @note The slots will be called in the order they were
+ /// connected to the signal.
+ ///
+ /// @param args Arguments that will be propagated to the
+ /// connected slots when they are called.
+ void operator()( A const&... args ) const {
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ slot( args... );
+ }
+ }
+ }
+
+ /// Construct a accumulator proxy object for the signal.
+ ///
+ /// The intended purpose of this function is to create a function
+ /// object that can be used to trigger the signal and accumulate
+ /// all the slot return values.
+ ///
+ /// The algorithm used to accumulate slot return values is similar
+ /// to `std::accumulate`. A given binary function is called for
+ /// each return value with the parameters consisting of the
+ /// return value of the accumulator function applied to the
+ /// previous slots return value, and the current slots return value.
+ /// A initial value must be provided for the first slot return type.
+ ///
+ /// @note This can only be used on signals that have slots with
+ /// non-void return types, since we can't accumulate void
+ /// values.
+ ///
+ /// @tparam T The type of the initial value given to the accumulator.
+ /// @tparam F The accumulator function type.
+ /// @param init Initial value given to the accumulator.
+ /// @param op Binary operator function object to apply by the accumulator.
+ /// The signature of the function should be
+ /// equivalent of the following:
+ /// `R func( T1 const& a, T2 const& b )`
+ /// - The signature does not need to have `const&`.
+ /// - The initial value, type `T`, must be implicitly
+ /// convertible to `R`
+ /// - The return type `R` must be implicitly convertible
+ /// to type `T1`.
+ /// - The type `R` must be `CopyAssignable`.
+ /// - The type `S::slot_type::result_type` (return type of
+ /// the signals slots) must be implicitly convertible to
+ /// type `T2`.
+ template <class T, class F>
+ signal_accumulator<signal_type, T, F, A...> accumulate( T init, F op ) const {
+ static_assert( std::is_same<R,void>::value == false, "Unable to accumulate slot return values with 'void' as return type." );
+ return { *this, init, op };
+ }
+
+
+ /// Trigger the signal, calling the slots and aggregate all
+ /// the slot return values into a container.
+ ///
+ /// @tparam C The type of container. This type must be
+ /// `DefaultConstructible`, and usable with
+ /// `std::back_insert_iterator`. Additionally it
+ /// must be either copyable or moveable.
+ /// @param args The arguments to propagate to the slots.
+ template <class C>
+ C aggregate( A const&... args ) const {
+ static_assert( std::is_same<R,void>::value == false, "Unable to aggregate slot return values with 'void' as return type." );
+ C container;
+ auto iterator = std::back_inserter( container );
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ (*iterator) = slot( args... );
+ }
+ }
+ return container;
+ }
+
+ /// Count the number of slots connected to this signal
+ /// @returns The number of connected slots
+ size_type slot_count() const {
+ return _slot_count;
+ }
+
+ /// Determine if the signal is empty, i.e. no slots are connected
+ /// to it.
+ /// @returns `true` is returned if the signal has no connected
+ /// slots, and `false` otherwise.
+ bool empty() const {
+ return slot_count() == 0;
+ }
+
+ /// Disconnects all slots
+ /// @note This operation invalidates all scoped_connection objects
+ void disconnect_all_slots() {
+ mutex_lock_type lock{ _mutex };
+ _slots.clear();
+ _slot_count = 0;
+ invalidate_disconnector();
+ }
+
+ private:
+ template<class, class, class, class...> friend class signal_accumulator;
+ /// Thread policy currently in use
+ using thread_policy = P;
+ /// Type of mutex, provided by threading policy
+ using mutex_type = typename thread_policy::mutex_type;
+ /// Type of mutex lock, provided by threading policy
+ using mutex_lock_type = typename thread_policy::mutex_lock_type;
+
+ /// Invalidate the internal disconnector object in a way
+ /// that is safe according to the current thread policy.
+ ///
+ /// This will effectively make all current connection objects to
+ /// to this signal incapable of disconnecting, since they keep a
+ /// weak pointer to the shared disconnector object.
+ void invalidate_disconnector() {
+ // If we are unlucky, some of the connected slots
+ // might be in the process of disconnecting from other threads.
+ // If this happens, we are risking to destruct the disconnector
+ // object managed by our shared pointer before they are done
+ // disconnecting. This would be bad. To solve this problem, we
+ // discard the shared pointer (that is pointing to the disconnector
+ // object within our own instance), but keep a weak pointer to that
+ // instance. We then stall the destruction until all other weak
+ // pointers have released their "lock" (indicated by the fact that
+ // we will get a nullptr when locking our weak pointer).
+ std::weak_ptr<detail::disconnector> weak{_shared_disconnector};
+ _shared_disconnector.reset();
+ while( weak.lock() != nullptr ) {
+ // we just yield here, allowing the OS to reschedule. We do
+ // this until all threads has released the disconnector object.
+ thread_policy::yield_thread();
+ }
+ }
+
+ /// Retrieve a copy of the current slots
+ ///
+ /// It's useful and necessary to copy the slots so we don't need
+ /// to hold the lock while calling the slots. If we hold the lock
+ /// we prevent the called slots from modifying the slots vector.
+ /// This simple "double buffering" will allow slots to disconnect
+ /// themself or other slots and connect new slots.
+ std::vector<slot_type> copy_slots() const
+ {
+ mutex_lock_type lock{ _mutex };
+ return _slots;
+ }
+
+ /// Implementation of the signal accumulator function call
+ template <class T, class F>
+ typename signal_accumulator<signal_type, T, F, A...>::result_type trigger_with_accumulator( T value, F& func, A const&... args ) const {
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ value = func( value, slot( args... ) );
+ }
+ }
+ return value;
+ }
+
+ /// Implementation of the disconnection operation.
+ ///
+ /// This is private, and only called by the connection
+ /// objects created when connecting slots to this signal.
+ /// @param index The slot index of the slot that should
+ /// be disconnected.
+ void disconnect( std::size_t index ) {
+ mutex_lock_type lock( _mutex );
+ assert( _slots.size() > index );
+ if( _slots[ index ] != nullptr ) {
+ --_slot_count;
+ }
+ _slots[ index ] = slot_type{};
+ while( _slots.size()>0 && !_slots.back() ) {
+ _slots.pop_back();
+ }
+ }
+
+ /// Implementation of the shared disconnection state
+ /// used by all connection created by signal instances.
+ ///
+ /// This inherits the @ref detail::disconnector interface
+ /// for type erasure.
+ struct disconnector :
+ detail::disconnector
+ {
+ /// Default constructor, resulting in a no-op disconnector.
+ disconnector() :
+ _ptr(nullptr)
+ {}
+
+ /// Create a disconnector that works with a given signal instance.
+ /// @param ptr Pointer to the signal instance that the disconnector
+ /// should work with.
+ disconnector( signal_type<P,R(A...)>* ptr ) :
+ _ptr( ptr )
+ {}
+
+ /// Disconnect a given slot on the current signal instance.
+ /// @note If the instance is default constructed, or created
+ /// with `nullptr` as signal pointer this operation will
+ /// effectively be a no-op.
+ /// @param index The index of the slot to disconnect.
+ void operator()( std::size_t index ) const override {
+ if( _ptr ) {
+ _ptr->disconnect( index );
+ }
+ }
+
+ /// Pointer to the current signal.
+ signal_type<P,R(A...)>* _ptr;
+ };
+
+ /// Mutex to synchronize access to the slot vector
+ mutable mutex_type _mutex;
+ /// Vector of all connected slots
+ std::vector<slot_type> _slots;
+ /// Number of connected slots
+ size_type _slot_count;
+ /// Disconnector operation, used for executing disconnection in a
+ /// type erased manner.
+ disconnector _disconnector;
+ /// Shared pointer to the disconnector. All connection objects has a
+ /// weak pointer to this pointer for performing disconnections.
+ std::shared_ptr<detail::disconnector> _shared_disconnector;
+ };
+
+ // Implementation of the disconnect operation of the connection class
+ inline void connection::disconnect() {
+ auto ptr = _weak_disconnector.lock();
+ if( ptr ) {
+ (*ptr)( _index );
+ }
+ _weak_disconnector.reset();
+ }
+
+ /// Signal type that is safe to use in multithreaded environments,
+ /// where the signal and slots exists in different threads.
+ /// The multithreaded policy provides mutexes and locks to synchronize
+ /// access to the signals internals.
+ ///
+ /// This is the recommended signal type, even for single threaded
+ /// environments.
+ template <class T> using signal = signal_type<multithread_policy, T>;
+
+ /// Signal type that is unsafe in multithreaded environments.
+ /// No synchronizations are provided to the signal_type for accessing
+ /// the internals.
+ ///
+ /// Only use this signal type if you are sure that your environment is
+ /// single threaded and performance is of importance.
+ template <class T> using unsafe_signal = signal_type<singlethread_policy, T>;
+} // namespace nod
+
+#endif // IG_NOD_INCLUDE_NOD_HPP