1 #ifndef IG_NOD_INCLUDE_NOD_HPP
2 #define IG_NOD_INCLUDE_NOD_HPP
4 #include <vector> // std::vector
5 #include <functional> // std::function
6 #include <mutex> // std::mutex, std::lock_guard
7 #include <memory> // std::shared_ptr, std::weak_ptr
8 #include <algorithm> // std::find_if()
9 #include <cassert> // assert()
10 #include <thread> // std::this_thread::yield()
11 #include <type_traits> // std::is_same
12 #include <iterator> // std::back_inserter
15 // implementational details
17 /// Interface for type erasure when disconnecting slots
19 virtual void operator()( std::size_t index ) const = 0;
21 /// Deleter that doesn't delete
22 inline void no_delete(disconnector*){
26 /// Base template for the signal class
27 template <class P, class T>
33 /// This is used to be able to disconnect slots after they have been connected.
34 /// Used as return type for the connect method of the signals.
36 /// Connections are default constructible.
37 /// Connections are not copy constructible or copy assignable.
38 /// Connections are move constructible and move assignable.
42 /// Default constructor
47 // Connection are not copy constructible or copy assignable
48 connection( connection const& ) = delete;
49 connection& operator=( connection const& ) = delete;
52 /// @param other The instance to move from.
53 connection( connection&& other ) :
54 _weak_disconnector( std::move(other._weak_disconnector) ),
55 _index( other._index )
58 /// Move assign operator.
59 /// @param other The instance to move from.
60 connection& operator=( connection&& other ) {
61 _weak_disconnector = std::move( other._weak_disconnector );
62 _index = other._index;
66 /// @returns `true` if the connection is connected to a signal object,
67 /// and `false` otherwise.
68 bool connected() const {
69 return !_weak_disconnector.expired();
72 /// Disconnect the slot from the connection.
74 /// If the connection represents a slot that is connected to a signal object, calling
75 /// this method will disconnect the slot from that object. The result of this operation
76 /// is that the slot will stop receiving calls when the signal is invoked.
80 /// The signal template is a friend of the connection, since it is the
81 /// only one allowed to create instances using the meaningful constructor.
82 template<class P,class T> friend class signal_type;
84 /// Create a connection.
85 /// @param shared_disconnector Disconnector instance that will be used to disconnect
86 /// the connection when the time comes. A weak pointer
87 /// to the disconnector will be held within the connection
89 /// @param index The slot index of the connection.
90 connection( std::shared_ptr<detail::disconnector> const& shared_disconnector, std::size_t index ) :
91 _weak_disconnector( shared_disconnector ),
95 /// Weak pointer to the current disconnector functor.
96 std::weak_ptr<detail::disconnector> _weak_disconnector;
97 /// Slot index of the connected slot.
101 /// Scoped connection class.
103 /// This type of connection is automatically disconnected when
104 /// the connection object is destructed.
106 class scoped_connection
109 /// Scoped are default constructible
110 scoped_connection() = default;
111 /// Scoped connections are not copy constructible
112 scoped_connection( scoped_connection const& ) = delete;
113 /// Scoped connections are not copy assingable
114 scoped_connection& operator=( scoped_connection const& ) = delete;
117 scoped_connection( scoped_connection&& other ) :
118 _connection( std::move(other._connection) )
121 /// Move assign operator.
122 /// @param other The instance to move from.
123 scoped_connection& operator=( scoped_connection&& other ) {
124 reset( std::move( other._connection ) );
128 /// Construct a scoped connection from a connection object
129 /// @param connection The connection object to manage
130 scoped_connection( connection&& c ) :
131 _connection( std::forward<connection>(c) )
135 ~scoped_connection() {
139 /// Assignment operator moving a new connection into the instance.
140 /// @note If the scoped_connection instance already contains a
141 /// connection, that connection will be disconnected as if
142 /// the scoped_connection was destroyed.
143 /// @param c New connection to manage
144 scoped_connection& operator=( connection&& c ) {
145 reset( std::forward<connection>(c) );
149 /// Reset the underlying connection to another connection.
150 /// @note The connection currently managed by the scoped_connection
151 /// instance will be disconnected when resetting.
152 /// @param c New connection to manage
153 void reset( connection&& c = {} ) {
155 _connection = std::move(c);
158 /// Release the underlying connection, without disconnecting it.
159 /// @returns The newly released connection instance is returned.
160 connection release() {
161 connection c = std::move(_connection);
162 _connection = connection{};
167 /// @returns `true` if the connection is connected to a signal object,
168 /// and `false` otherwise.
169 bool connected() const {
170 return _connection.connected();
173 /// Disconnect the slot from the connection.
175 /// If the connection represents a slot that is connected to a signal object, calling
176 /// this method will disconnect the slot from that object. The result of this operation
177 /// is that the slot will stop receiving calls when the signal is invoked.
179 _connection.disconnect();
183 /// Underlying connection object
184 connection _connection;
187 /// Policy for multi threaded use of signals.
189 /// This policy provides mutex and lock types for use in
190 /// a multithreaded environment, where signals and slots
191 /// may exists in different threads.
193 /// This policy is used in the `nod::signal` type provided
195 struct multithread_policy
197 using mutex_type = std::mutex;
198 using mutex_lock_type = std::unique_lock<mutex_type>;
199 /// Function that yields the current thread, allowing
200 /// the OS to reschedule.
201 static void yield_thread() {
202 std::this_thread::yield();
204 /// Function that defers a lock to a lock function that prevents deadlock
205 static mutex_lock_type defer_lock(mutex_type & m){
206 return mutex_lock_type{m, std::defer_lock};
208 /// Function that locks two mutexes and prevents deadlock
209 static void lock(mutex_lock_type & a,mutex_lock_type & b) {
214 /// Policy for single threaded use of signals.
216 /// This policy provides dummy implementations for mutex
217 /// and lock types, resulting in that no synchronization
220 /// This policy is used in the `nod::unsafe_signal` type
221 /// provided by the library.
222 struct singlethread_policy
224 /// Dummy mutex type that doesn't do anything
226 /// Dummy lock type, that doesn't do any locking.
227 struct mutex_lock_type
229 /// A lock type must be constructible from a
230 /// mutex type from the same thread policy.
231 explicit mutex_lock_type( mutex_type const& ) {
234 /// Dummy implementation of thread yielding, that
235 /// doesn't do any actual yielding.
236 static void yield_thread() {
238 /// Dummy implemention of defer_lock that doesn't
240 static mutex_lock_type defer_lock(mutex_type &m){
241 return mutex_lock_type{m};
243 /// Dummy implemention of lock that doesn't
245 static void lock(mutex_lock_type &,mutex_lock_type &) {
249 /// Signal accumulator class template.
251 /// This acts sort of as a proxy for triggering a signal and
252 /// accumulating the slot return values.
254 /// This class is not really intended to instantiate by client code.
255 /// Instances are aquired as return values of the method `accumulate()`
256 /// called on signals.
258 /// @tparam S Type of signal. The signal_accumulator acts
259 /// as a type of proxy for a signal instance of
261 /// @tparam T Type of initial value of the accumulate algorithm.
262 /// This type must meet the requirements of `CopyAssignable`
263 /// and `CopyConstructible`
264 /// @tparam F Type of accumulation function.
265 /// @tparam A... Argument types of the underlying signal type.
267 template <class S, class T, class F, class...A>
268 class signal_accumulator
271 /// Result type when calling the accumulating function operator.
272 #if __cplusplus >= 201703L
273 using result_type = typename std::invoke_result<F, T, typename S::slot_type::result_type>::type;
275 using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::type;
278 /// Construct a signal_accumulator as a proxy to a given signal
280 /// @param signal Signal instance.
281 /// @param init Initial value of the accumulate algorithm.
282 /// @param func Binary operation function object that will be
283 /// applied to all slot return values.
284 /// The signature of the function should be
285 /// equivalent of the following:
286 /// `R func( T1 const& a, T2 const& b )`
287 /// - The signature does not need to have `const&`.
288 /// - The initial value, type `T`, must be implicitly
289 /// convertible to `R`
290 /// - The return type `R` must be implicitly convertible
292 /// - The type `R` must be `CopyAssignable`.
293 /// - The type `S::slot_type::result_type` (return type of
294 /// the signals slots) must be implicitly convertible to
296 signal_accumulator( S const& signal, T init, F func ) :
302 /// Function call operator.
304 /// Calling this will trigger the underlying signal and accumulate
305 /// all of the connected slots return values with the current
306 /// initial value and accumulator function.
308 /// When called, this will invoke the accumulator function will
309 /// be called for each return value of the slots. The semantics
310 /// are similar to the `std::accumulate` algorithm.
312 /// @param args Arguments to propagate to the slots of the
313 /// underlying when triggering the signal.
314 result_type operator()( A const& ... args ) const {
315 return _signal.trigger_with_accumulator( _init, _func, args... );
320 /// Reference to the underlying signal to proxy.
322 /// Initial value of the accumulate algorithm.
324 /// Accumulator function.
329 /// Signal template specialization.
331 /// This is the main signal implementation, and it is used to
332 /// implement the observer pattern whithout the overhead
333 /// boilerplate code that typically comes with it.
335 /// Any function or function object is considered a slot, and
336 /// can be connected to a signal instance, as long as the signature
337 /// of the slot matches the signature of the signal.
339 /// @tparam P Threading policy for the signal.
340 /// A threading policy must provide two type definitions:
341 /// - P::mutex_type, this type will be used as a mutex
342 /// in the signal_type class template.
343 /// - P::mutex_lock_type, this type must implement a
344 /// constructor that takes a P::mutex_type as a parameter,
345 /// and it must have the semantics of a scoped mutex lock
346 /// like std::lock_guard, i.e. locking in the constructor
347 /// and unlocking in the destructor.
349 /// @tparam R Return value type of the slots connected to the signal.
350 /// @tparam A... Argument types of the slots connected to the signal.
351 template <class P, class R, class... A >
352 class signal_type<P,R(A...)>
355 /// signals are not copy constructible
356 signal_type( signal_type const& ) = delete;
357 /// signals are not copy assignable
358 signal_type& operator=( signal_type const& ) = delete;
359 /// signals are move constructible
360 signal_type(signal_type&& other)
362 mutex_lock_type lock{other._mutex};
363 _slot_count = std::move(other._slot_count);
364 _slots = std::move(other._slots);
365 if(other._shared_disconnector != nullptr)
367 _disconnector = disconnector{ this };
368 _shared_disconnector = std::move(other._shared_disconnector);
369 // replace the disconnector with our own disconnector
370 *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
373 /// signals are move assignable
374 signal_type& operator=(signal_type&& other)
376 auto lock = thread_policy::defer_lock(_mutex);
377 auto other_lock = thread_policy::defer_lock(other._mutex);
378 thread_policy::lock(lock,other_lock);
380 _slot_count = std::move(other._slot_count);
381 _slots = std::move(other._slots);
382 if(other._shared_disconnector != nullptr)
384 _disconnector = disconnector{ this };
385 _shared_disconnector = std::move(other._shared_disconnector);
386 // replace the disconnector with our own disconnector
387 *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
392 /// signals are default constructible
397 // Destruct the signal object.
399 invalidate_disconnector();
402 /// Type that will be used to store the slots for this signal type.
403 using slot_type = std::function<R(A...)>;
404 /// Type that is used for counting the slots connected to this signal.
405 using size_type = typename std::vector<slot_type>::size_type;
408 /// Connect a new slot to the signal.
410 /// The connected slot will be called every time the signal
412 /// @param slot The slot to connect. This must be a callable with
413 /// the same signature as the signal itself.
414 /// @return A connection object is returned, and can be used to
415 /// disconnect the slot.
417 connection connect( T&& slot ) {
418 mutex_lock_type lock{ _mutex };
419 _slots.push_back( std::forward<T>(slot) );
420 std::size_t index = _slots.size()-1;
421 if( _shared_disconnector == nullptr ) {
422 _disconnector = disconnector{ this };
423 _shared_disconnector = std::shared_ptr<detail::disconnector>{&_disconnector, detail::no_delete};
426 return connection{ _shared_disconnector, index };
429 /// Function call operator.
431 /// Calling this is how the signal is triggered and the
432 /// connected slots are called.
434 /// @note The slots will be called in the order they were
435 /// connected to the signal.
437 /// @param args Arguments that will be propagated to the
438 /// connected slots when they are called.
439 void operator()( A const&... args ) const {
440 for( auto const& slot : copy_slots() ) {
447 /// Construct a accumulator proxy object for the signal.
449 /// The intended purpose of this function is to create a function
450 /// object that can be used to trigger the signal and accumulate
451 /// all the slot return values.
453 /// The algorithm used to accumulate slot return values is similar
454 /// to `std::accumulate`. A given binary function is called for
455 /// each return value with the parameters consisting of the
456 /// return value of the accumulator function applied to the
457 /// previous slots return value, and the current slots return value.
458 /// A initial value must be provided for the first slot return type.
460 /// @note This can only be used on signals that have slots with
461 /// non-void return types, since we can't accumulate void
464 /// @tparam T The type of the initial value given to the accumulator.
465 /// @tparam F The accumulator function type.
466 /// @param init Initial value given to the accumulator.
467 /// @param op Binary operator function object to apply by the accumulator.
468 /// The signature of the function should be
469 /// equivalent of the following:
470 /// `R func( T1 const& a, T2 const& b )`
471 /// - The signature does not need to have `const&`.
472 /// - The initial value, type `T`, must be implicitly
473 /// convertible to `R`
474 /// - The return type `R` must be implicitly convertible
476 /// - The type `R` must be `CopyAssignable`.
477 /// - The type `S::slot_type::result_type` (return type of
478 /// the signals slots) must be implicitly convertible to
480 template <class T, class F>
481 signal_accumulator<signal_type, T, F, A...> accumulate( T init, F op ) const {
482 static_assert( std::is_same<R,void>::value == false, "Unable to accumulate slot return values with 'void' as return type." );
483 return { *this, init, op };
487 /// Trigger the signal, calling the slots and aggregate all
488 /// the slot return values into a container.
490 /// @tparam C The type of container. This type must be
491 /// `DefaultConstructible`, and usable with
492 /// `std::back_insert_iterator`. Additionally it
493 /// must be either copyable or moveable.
494 /// @param args The arguments to propagate to the slots.
496 C aggregate( A const&... args ) const {
497 static_assert( std::is_same<R,void>::value == false, "Unable to aggregate slot return values with 'void' as return type." );
499 auto iterator = std::back_inserter( container );
500 for( auto const& slot : copy_slots() ) {
502 (*iterator) = slot( args... );
508 /// Count the number of slots connected to this signal
509 /// @returns The number of connected slots
510 size_type slot_count() const {
514 /// Determine if the signal is empty, i.e. no slots are connected
516 /// @returns `true` is returned if the signal has no connected
517 /// slots, and `false` otherwise.
519 return slot_count() == 0;
522 /// Disconnects all slots
523 /// @note This operation invalidates all scoped_connection objects
524 void disconnect_all_slots() {
525 mutex_lock_type lock{ _mutex };
528 invalidate_disconnector();
532 template<class, class, class, class...> friend class signal_accumulator;
533 /// Thread policy currently in use
534 using thread_policy = P;
535 /// Type of mutex, provided by threading policy
536 using mutex_type = typename thread_policy::mutex_type;
537 /// Type of mutex lock, provided by threading policy
538 using mutex_lock_type = typename thread_policy::mutex_lock_type;
540 /// Invalidate the internal disconnector object in a way
541 /// that is safe according to the current thread policy.
543 /// This will effectively make all current connection objects to
544 /// to this signal incapable of disconnecting, since they keep a
545 /// weak pointer to the shared disconnector object.
546 void invalidate_disconnector() {
547 // If we are unlucky, some of the connected slots
548 // might be in the process of disconnecting from other threads.
549 // If this happens, we are risking to destruct the disconnector
550 // object managed by our shared pointer before they are done
551 // disconnecting. This would be bad. To solve this problem, we
552 // discard the shared pointer (that is pointing to the disconnector
553 // object within our own instance), but keep a weak pointer to that
554 // instance. We then stall the destruction until all other weak
555 // pointers have released their "lock" (indicated by the fact that
556 // we will get a nullptr when locking our weak pointer).
557 std::weak_ptr<detail::disconnector> weak{_shared_disconnector};
558 _shared_disconnector.reset();
559 while( weak.lock() != nullptr ) {
560 // we just yield here, allowing the OS to reschedule. We do
561 // this until all threads has released the disconnector object.
562 thread_policy::yield_thread();
566 /// Retrieve a copy of the current slots
568 /// It's useful and necessary to copy the slots so we don't need
569 /// to hold the lock while calling the slots. If we hold the lock
570 /// we prevent the called slots from modifying the slots vector.
571 /// This simple "double buffering" will allow slots to disconnect
572 /// themself or other slots and connect new slots.
573 std::vector<slot_type> copy_slots() const
575 mutex_lock_type lock{ _mutex };
579 /// Implementation of the signal accumulator function call
580 template <class T, class F>
581 typename signal_accumulator<signal_type, T, F, A...>::result_type trigger_with_accumulator( T value, F& func, A const&... args ) const {
582 for( auto const& slot : copy_slots() ) {
584 value = func( value, slot( args... ) );
590 /// Implementation of the disconnection operation.
592 /// This is private, and only called by the connection
593 /// objects created when connecting slots to this signal.
594 /// @param index The slot index of the slot that should
596 void disconnect( std::size_t index ) {
597 mutex_lock_type lock( _mutex );
598 assert( _slots.size() > index );
599 if( _slots[ index ] != nullptr ) {
602 _slots[ index ] = slot_type{};
603 while( _slots.size()>0 && !_slots.back() ) {
608 /// Implementation of the shared disconnection state
609 /// used by all connection created by signal instances.
611 /// This inherits the @ref detail::disconnector interface
612 /// for type erasure.
613 struct disconnector :
616 /// Default constructor, resulting in a no-op disconnector.
621 /// Create a disconnector that works with a given signal instance.
622 /// @param ptr Pointer to the signal instance that the disconnector
623 /// should work with.
624 disconnector( signal_type<P,R(A...)>* ptr ) :
628 /// Disconnect a given slot on the current signal instance.
629 /// @note If the instance is default constructed, or created
630 /// with `nullptr` as signal pointer this operation will
631 /// effectively be a no-op.
632 /// @param index The index of the slot to disconnect.
633 void operator()( std::size_t index ) const override {
635 _ptr->disconnect( index );
639 /// Pointer to the current signal.
640 signal_type<P,R(A...)>* _ptr;
643 /// Mutex to synchronize access to the slot vector
644 mutable mutex_type _mutex;
645 /// Vector of all connected slots
646 std::vector<slot_type> _slots;
647 /// Number of connected slots
648 size_type _slot_count;
649 /// Disconnector operation, used for executing disconnection in a
650 /// type erased manner.
651 disconnector _disconnector;
652 /// Shared pointer to the disconnector. All connection objects has a
653 /// weak pointer to this pointer for performing disconnections.
654 std::shared_ptr<detail::disconnector> _shared_disconnector;
657 // Implementation of the disconnect operation of the connection class
658 inline void connection::disconnect() {
659 auto ptr = _weak_disconnector.lock();
663 _weak_disconnector.reset();
666 /// Signal type that is safe to use in multithreaded environments,
667 /// where the signal and slots exists in different threads.
668 /// The multithreaded policy provides mutexes and locks to synchronize
669 /// access to the signals internals.
671 /// This is the recommended signal type, even for single threaded
673 template <class T> using signal = signal_type<multithread_policy, T>;
675 /// Signal type that is unsafe in multithreaded environments.
676 /// No synchronizations are provided to the signal_type for accessing
679 /// Only use this signal type if you are sure that your environment is
680 /// single threaded and performance is of importance.
681 template <class T> using unsafe_signal = signal_type<singlethread_policy, T>;
684 #endif // IG_NOD_INCLUDE_NOD_HPP