You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

631 lines
23 KiB

5 months ago
  1. #ifndef IG_NOD_INCLUDE_NOD_HPP
  2. #define IG_NOD_INCLUDE_NOD_HPP
  3. #include <vector> // std::vector
  4. #include <functional> // std::function
  5. #include <mutex> // std::mutex, std::lock_guard
  6. #include <memory> // std::shared_ptr, std::weak_ptr
  7. #include <algorithm> // std::find_if()
  8. #include <cassert> // assert()
  9. #include <thread> // std::this_thread::yield()
  10. #include <type_traits> // std::is_same
  11. #include <iterator> // std::back_inserter
  12. namespace nod {
  13. // implementational details
  14. namespace detail {
  15. /// Interface for type erasure when disconnecting slots
  16. struct disconnector {
  17. virtual void operator()( std::size_t index ) const = 0;
  18. };
  19. /// Deleter that doesn't delete
  20. inline void no_delete(disconnector*){
  21. };
  22. } // namespace detail
  23. /// Base template for the signal class
  24. template <class P, class T>
  25. class signal_type;
  26. /// Connection class.
  27. ///
  28. /// This is used to be able to disconnect slots after they have been connected.
  29. /// Used as return type for the connect method of the signals.
  30. ///
  31. /// Connections are default constructible.
  32. /// Connections are not copy constructible or copy assignable.
  33. /// Connections are move constructible and move assignable.
  34. ///
  35. class connection {
  36. public:
  37. /// Default constructor
  38. connection() :
  39. _index()
  40. {}
  41. // Connection are not copy constructible or copy assignable
  42. connection( connection const& ) = delete;
  43. connection& operator=( connection const& ) = delete;
  44. /// Move constructor
  45. /// @param other The instance to move from.
  46. connection( connection&& other ) :
  47. _weak_disconnector( std::move(other._weak_disconnector) ),
  48. _index( other._index )
  49. {}
  50. /// Move assign operator.
  51. /// @param other The instance to move from.
  52. connection& operator=( connection&& other ) {
  53. _weak_disconnector = std::move( other._weak_disconnector );
  54. _index = other._index;
  55. return *this;
  56. }
  57. /// @returns `true` if the connection is connected to a signal object,
  58. /// and `false` otherwise.
  59. bool connected() const {
  60. return !_weak_disconnector.expired();
  61. }
  62. /// Disconnect the slot from the connection.
  63. ///
  64. /// If the connection represents a slot that is connected to a signal object, calling
  65. /// this method will disconnect the slot from that object. The result of this operation
  66. /// is that the slot will stop receiving calls when the signal is invoked.
  67. void disconnect();
  68. private:
  69. /// The signal template is a friend of the connection, since it is the
  70. /// only one allowed to create instances using the meaningful constructor.
  71. template<class P,class T> friend class signal_type;
  72. /// Create a connection.
  73. /// @param shared_disconnector Disconnector instance that will be used to disconnect
  74. /// the connection when the time comes. A weak pointer
  75. /// to the disconnector will be held within the connection
  76. /// object.
  77. /// @param index The slot index of the connection.
  78. connection( std::shared_ptr<detail::disconnector> const& shared_disconnector, std::size_t index ) :
  79. _weak_disconnector( shared_disconnector ),
  80. _index( index )
  81. {}
  82. /// Weak pointer to the current disconnector functor.
  83. std::weak_ptr<detail::disconnector> _weak_disconnector;
  84. /// Slot index of the connected slot.
  85. std::size_t _index;
  86. };
  87. /// Scoped connection class.
  88. ///
  89. /// This type of connection is automatically disconnected when
  90. /// the connection object is destructed.
  91. ///
  92. class scoped_connection
  93. {
  94. public:
  95. /// Scoped are default constructible
  96. scoped_connection() = default;
  97. /// Scoped connections are not copy constructible
  98. scoped_connection( scoped_connection const& ) = delete;
  99. /// Scoped connections are not copy assingable
  100. scoped_connection& operator=( scoped_connection const& ) = delete;
  101. /// Move constructor
  102. scoped_connection( scoped_connection&& other ) :
  103. _connection( std::move(other._connection) )
  104. {}
  105. /// Move assign operator.
  106. /// @param other The instance to move from.
  107. scoped_connection& operator=( scoped_connection&& other ) {
  108. reset( std::move( other._connection ) );
  109. return *this;
  110. }
  111. /// Construct a scoped connection from a connection object
  112. /// @param connection The connection object to manage
  113. scoped_connection( connection&& c ) :
  114. _connection( std::forward<connection>(c) )
  115. {}
  116. /// destructor
  117. ~scoped_connection() {
  118. disconnect();
  119. }
  120. /// Assignment operator moving a new connection into the instance.
  121. /// @note If the scoped_connection instance already contains a
  122. /// connection, that connection will be disconnected as if
  123. /// the scoped_connection was destroyed.
  124. /// @param c New connection to manage
  125. scoped_connection& operator=( connection&& c ) {
  126. reset( std::forward<connection>(c) );
  127. return *this;
  128. }
  129. /// Reset the underlying connection to another connection.
  130. /// @note The connection currently managed by the scoped_connection
  131. /// instance will be disconnected when resetting.
  132. /// @param c New connection to manage
  133. void reset( connection&& c = {} ) {
  134. disconnect();
  135. _connection = std::move(c);
  136. }
  137. /// Release the underlying connection, without disconnecting it.
  138. /// @returns The newly released connection instance is returned.
  139. connection release() {
  140. connection c = std::move(_connection);
  141. _connection = connection{};
  142. return c;
  143. }
  144. ///
  145. /// @returns `true` if the connection is connected to a signal object,
  146. /// and `false` otherwise.
  147. bool connected() const {
  148. return _connection.connected();
  149. }
  150. /// Disconnect the slot from the connection.
  151. ///
  152. /// If the connection represents a slot that is connected to a signal object, calling
  153. /// this method will disconnect the slot from that object. The result of this operation
  154. /// is that the slot will stop receiving calls when the signal is invoked.
  155. void disconnect() {
  156. _connection.disconnect();
  157. }
  158. private:
  159. /// Underlying connection object
  160. connection _connection;
  161. };
  162. /// Policy for multi threaded use of signals.
  163. ///
  164. /// This policy provides mutex and lock types for use in
  165. /// a multithreaded environment, where signals and slots
  166. /// may exists in different threads.
  167. ///
  168. /// This policy is used in the `nod::signal` type provided
  169. /// by the library.
  170. struct multithread_policy
  171. {
  172. using mutex_type = std::mutex;
  173. using mutex_lock_type = std::lock_guard<mutex_type>;
  174. /// Function that yields the current thread, allowing
  175. /// the OS to reschedule.
  176. static void yield_thread() {
  177. std::this_thread::yield();
  178. }
  179. };
  180. /// Policy for single threaded use of signals.
  181. ///
  182. /// This policy provides dummy implementations for mutex
  183. /// and lock types, resulting in that no synchronization
  184. /// will take place.
  185. ///
  186. /// This policy is used in the `nod::unsafe_signal` type
  187. /// provided by the library.
  188. struct singlethread_policy
  189. {
  190. /// Dummy mutex type that doesn't do anything
  191. struct mutex_type{};
  192. /// Dummy lock type, that doesn't do any locking.
  193. struct mutex_lock_type
  194. {
  195. /// A lock type must be constructible from a
  196. /// mutex type from the same thread policy.
  197. explicit mutex_lock_type( mutex_type const& ) {
  198. }
  199. };
  200. /// Dummy implementation of thread yielding, that
  201. /// doesn't do any actual yielding.
  202. static void yield_thread() {
  203. }
  204. };
  205. /// Signal accumulator class template.
  206. ///
  207. /// This acts sort of as a proxy for triggering a signal and
  208. /// accumulating the slot return values.
  209. ///
  210. /// This class is not really intended to instantiate by client code.
  211. /// Instances are aquired as return values of the method `accumulate()`
  212. /// called on signals.
  213. ///
  214. /// @tparam S Type of signal. The signal_accumulator acts
  215. /// as a type of proxy for a signal instance of
  216. /// this type.
  217. /// @tparam T Type of initial value of the accumulate algorithm.
  218. /// This type must meet the requirements of `CopyAssignable`
  219. /// and `CopyConstructible`
  220. /// @tparam F Type of accumulation function.
  221. /// @tparam A... Argument types of the underlying signal type.
  222. ///
  223. template <class S, class T, class F, class...A>
  224. class signal_accumulator
  225. {
  226. public:
  227. /// Result type when calling the accumulating function operator.
  228. using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::type;
  229. /// Construct a signal_accumulator as a proxy to a given signal
  230. //
  231. /// @param signal Signal instance.
  232. /// @param init Initial value of the accumulate algorithm.
  233. /// @param func Binary operation function object that will be
  234. /// applied to all slot return values.
  235. /// The signature of the function should be
  236. /// equivalent of the following:
  237. /// `R func( T1 const& a, T2 const& b )`
  238. /// - The signature does not need to have `const&`.
  239. /// - The initial value, type `T`, must be implicitly
  240. /// convertible to `R`
  241. /// - The return type `R` must be implicitly convertible
  242. /// to type `T1`.
  243. /// - The type `R` must be `CopyAssignable`.
  244. /// - The type `S::slot_type::result_type` (return type of
  245. /// the signals slots) must be implicitly convertible to
  246. /// type `T2`.
  247. signal_accumulator( S const& signal, T init, F func ) :
  248. _signal( signal ),
  249. _init( init ),
  250. _func( func )
  251. {}
  252. /// Function call operator.
  253. ///
  254. /// Calling this will trigger the underlying signal and accumulate
  255. /// all of the connected slots return values with the current
  256. /// initial value and accumulator function.
  257. ///
  258. /// When called, this will invoke the accumulator function will
  259. /// be called for each return value of the slots. The semantics
  260. /// are similar to the `std::accumulate` algorithm.
  261. ///
  262. /// @param args Arguments to propagate to the slots of the
  263. /// underlying when triggering the signal.
  264. result_type operator()( A const& ... args ) const {
  265. return _signal.trigger_with_accumulator( _init, _func, args... );
  266. }
  267. private:
  268. /// Reference to the underlying signal to proxy.
  269. S const& _signal;
  270. /// Initial value of the accumulate algorithm.
  271. T _init;
  272. /// Accumulator function.
  273. F _func;
  274. };
  275. /// Signal template specialization.
  276. ///
  277. /// This is the main signal implementation, and it is used to
  278. /// implement the observer pattern whithout the overhead
  279. /// boilerplate code that typically comes with it.
  280. ///
  281. /// Any function or function object is considered a slot, and
  282. /// can be connected to a signal instance, as long as the signature
  283. /// of the slot matches the signature of the signal.
  284. ///
  285. /// @tparam P Threading policy for the signal.
  286. /// A threading policy must provide two type definitions:
  287. /// - P::mutex_type, this type will be used as a mutex
  288. /// in the signal_type class template.
  289. /// - P::mutex_lock_type, this type must implement a
  290. /// constructor that takes a P::mutex_type as a parameter,
  291. /// and it must have the semantics of a scoped mutex lock
  292. /// like std::lock_guard, i.e. locking in the constructor
  293. /// and unlocking in the destructor.
  294. ///
  295. /// @tparam R Return value type of the slots connected to the signal.
  296. /// @tparam A... Argument types of the slots connected to the signal.
  297. template <class P, class R, class... A >
  298. class signal_type<P,R(A...)>
  299. {
  300. public:
  301. /// signals are not copy constructible
  302. signal_type( signal_type const& ) = delete;
  303. /// signals are not copy assignable
  304. signal_type& operator=( signal_type const& ) = delete;
  305. /// signals are default constructible
  306. signal_type() :
  307. _slot_count(0)
  308. {}
  309. // Destruct the signal object.
  310. ~signal_type() {
  311. invalidate_disconnector();
  312. }
  313. /// Type that will be used to store the slots for this signal type.
  314. using slot_type = std::function<R(A...)>;
  315. /// Type that is used for counting the slots connected to this signal.
  316. using size_type = typename std::vector<slot_type>::size_type;
  317. /// Connect a new slot to the signal.
  318. ///
  319. /// The connected slot will be called every time the signal
  320. /// is triggered.
  321. /// @param slot The slot to connect. This must be a callable with
  322. /// the same signature as the signal itself.
  323. /// @return A connection object is returned, and can be used to
  324. /// disconnect the slot.
  325. template <class T>
  326. connection connect( T&& slot ) {
  327. mutex_lock_type lock{ _mutex };
  328. _slots.push_back( std::forward<T>(slot) );
  329. std::size_t index = _slots.size()-1;
  330. if( _shared_disconnector == nullptr ) {
  331. _disconnector = disconnector{ this };
  332. _shared_disconnector = std::shared_ptr<detail::disconnector>{&_disconnector, detail::no_delete};
  333. }
  334. ++_slot_count;
  335. return connection{ _shared_disconnector, index };
  336. }
  337. /// Function call operator.
  338. ///
  339. /// Calling this is how the signal is triggered and the
  340. /// connected slots are called.
  341. ///
  342. /// @note The slots will be called in the order they were
  343. /// connected to the signal.
  344. ///
  345. /// @param args Arguments that will be propagated to the
  346. /// connected slots when they are called.
  347. void operator()( A const&... args ) const {
  348. for( auto const& slot : copy_slots() ) {
  349. if( slot ) {
  350. slot( args... );
  351. }
  352. }
  353. }
  354. /// Construct a accumulator proxy object for the signal.
  355. ///
  356. /// The intended purpose of this function is to create a function
  357. /// object that can be used to trigger the signal and accumulate
  358. /// all the slot return values.
  359. ///
  360. /// The algorithm used to accumulate slot return values is similar
  361. /// to `std::accumulate`. A given binary function is called for
  362. /// each return value with the parameters consisting of the
  363. /// return value of the accumulator function applied to the
  364. /// previous slots return value, and the current slots return value.
  365. /// A initial value must be provided for the first slot return type.
  366. ///
  367. /// @note This can only be used on signals that have slots with
  368. /// non-void return types, since we can't accumulate void
  369. /// values.
  370. ///
  371. /// @tparam T The type of the initial value given to the accumulator.
  372. /// @tparam F The accumulator function type.
  373. /// @param init Initial value given to the accumulator.
  374. /// @param op Binary operator function object to apply by the accumulator.
  375. /// The signature of the function should be
  376. /// equivalent of the following:
  377. /// `R func( T1 const& a, T2 const& b )`
  378. /// - The signature does not need to have `const&`.
  379. /// - The initial value, type `T`, must be implicitly
  380. /// convertible to `R`
  381. /// - The return type `R` must be implicitly convertible
  382. /// to type `T1`.
  383. /// - The type `R` must be `CopyAssignable`.
  384. /// - The type `S::slot_type::result_type` (return type of
  385. /// the signals slots) must be implicitly convertible to
  386. /// type `T2`.
  387. template <class T, class F>
  388. signal_accumulator<signal_type, T, F, A...> accumulate( T init, F op ) const {
  389. static_assert( std::is_same<R,void>::value == false, "Unable to accumulate slot return values with 'void' as return type." );
  390. return { *this, init, op };
  391. }
  392. /// Trigger the signal, calling the slots and aggregate all
  393. /// the slot return values into a container.
  394. ///
  395. /// @tparam C The type of container. This type must be
  396. /// `DefaultConstructible`, and usable with
  397. /// `std::back_insert_iterator`. Additionally it
  398. /// must be either copyable or moveable.
  399. /// @param args The arguments to propagate to the slots.
  400. template <class C>
  401. C aggregate( A const&... args ) const {
  402. static_assert( std::is_same<R,void>::value == false, "Unable to aggregate slot return values with 'void' as return type." );
  403. C container;
  404. auto iterator = std::back_inserter( container );
  405. for( auto const& slot : copy_slots() ) {
  406. if( slot ) {
  407. (*iterator) = slot( args... );
  408. }
  409. }
  410. return container;
  411. }
  412. /// Count the number of slots connected to this signal
  413. /// @returns The number of connected slots
  414. size_type slot_count() const {
  415. return _slot_count;
  416. }
  417. /// Determine if the signal is empty, i.e. no slots are connected
  418. /// to it.
  419. /// @returns `true` is returned if the signal has no connected
  420. /// slots, and `false` otherwise.
  421. bool empty() const {
  422. return slot_count() == 0;
  423. }
  424. /// Disconnects all slots
  425. /// @note This operation invalidates all scoped_connection objects
  426. void disconnect_all_slots() {
  427. mutex_lock_type lock{ _mutex };
  428. _slots.clear();
  429. _slot_count = 0;
  430. invalidate_disconnector();
  431. }
  432. private:
  433. template<class, class, class, class...> friend class signal_accumulator;
  434. /// Thread policy currently in use
  435. using thread_policy = P;
  436. /// Type of mutex, provided by threading policy
  437. using mutex_type = typename thread_policy::mutex_type;
  438. /// Type of mutex lock, provided by threading policy
  439. using mutex_lock_type = typename thread_policy::mutex_lock_type;
  440. /// Invalidate the internal disconnector object in a way
  441. /// that is safe according to the current thread policy.
  442. ///
  443. /// This will effectively make all current connection objects to
  444. /// to this signal incapable of disconnecting, since they keep a
  445. /// weak pointer to the shared disconnector object.
  446. void invalidate_disconnector() {
  447. // If we are unlucky, some of the connected slots
  448. // might be in the process of disconnecting from other threads.
  449. // If this happens, we are risking to destruct the disconnector
  450. // object managed by our shared pointer before they are done
  451. // disconnecting. This would be bad. To solve this problem, we
  452. // discard the shared pointer (that is pointing to the disconnector
  453. // object within our own instance), but keep a weak pointer to that
  454. // instance. We then stall the destruction until all other weak
  455. // pointers have released their "lock" (indicated by the fact that
  456. // we will get a nullptr when locking our weak pointer).
  457. std::weak_ptr<detail::disconnector> weak{_shared_disconnector};
  458. _shared_disconnector.reset();
  459. while( weak.lock() != nullptr ) {
  460. // we just yield here, allowing the OS to reschedule. We do
  461. // this until all threads has released the disconnector object.
  462. thread_policy::yield_thread();
  463. }
  464. }
  465. /// Retrieve a copy of the current slots
  466. ///
  467. /// It's useful and necessary to copy the slots so we don't need
  468. /// to hold the lock while calling the slots. If we hold the lock
  469. /// we prevent the called slots from modifying the slots vector.
  470. /// This simple "double buffering" will allow slots to disconnect
  471. /// themself or other slots and connect new slots.
  472. std::vector<slot_type> copy_slots() const
  473. {
  474. mutex_lock_type lock{ _mutex };
  475. return _slots;
  476. }
  477. /// Implementation of the signal accumulator function call
  478. template <class T, class F>
  479. typename signal_accumulator<signal_type, T, F, A...>::result_type trigger_with_accumulator( T value, F& func, A const&... args ) const {
  480. for( auto const& slot : copy_slots() ) {
  481. if( slot ) {
  482. value = func( value, slot( args... ) );
  483. }
  484. }
  485. return value;
  486. }
  487. /// Implementation of the disconnection operation.
  488. ///
  489. /// This is private, and only called by the connection
  490. /// objects created when connecting slots to this signal.
  491. /// @param index The slot index of the slot that should
  492. /// be disconnected.
  493. void disconnect( std::size_t index ) {
  494. mutex_lock_type lock( _mutex );
  495. assert( _slots.size() > index );
  496. if( _slots[ index ] != nullptr ) {
  497. --_slot_count;
  498. }
  499. _slots[ index ] = slot_type{};
  500. while( _slots.size()>0 && !_slots.back() ) {
  501. _slots.pop_back();
  502. }
  503. }
  504. /// Implementation of the shared disconnection state
  505. /// used by all connection created by signal instances.
  506. ///
  507. /// This inherits the @ref detail::disconnector interface
  508. /// for type erasure.
  509. struct disconnector :
  510. detail::disconnector
  511. {
  512. /// Default constructor, resulting in a no-op disconnector.
  513. disconnector() :
  514. _ptr(nullptr)
  515. {}
  516. /// Create a disconnector that works with a given signal instance.
  517. /// @param ptr Pointer to the signal instance that the disconnector
  518. /// should work with.
  519. disconnector( signal_type<P,R(A...)>* ptr ) :
  520. _ptr( ptr )
  521. {}
  522. /// Disconnect a given slot on the current signal instance.
  523. /// @note If the instance is default constructed, or created
  524. /// with `nullptr` as signal pointer this operation will
  525. /// effectively be a no-op.
  526. /// @param index The index of the slot to disconnect.
  527. void operator()( std::size_t index ) const override {
  528. if( _ptr ) {
  529. _ptr->disconnect( index );
  530. }
  531. }
  532. /// Pointer to the current signal.
  533. signal_type<P,R(A...)>* _ptr;
  534. };
  535. /// Mutex to synchronize access to the slot vector
  536. mutable mutex_type _mutex;
  537. /// Vector of all connected slots
  538. std::vector<slot_type> _slots;
  539. /// Number of connected slots
  540. size_type _slot_count;
  541. /// Disconnector operation, used for executing disconnection in a
  542. /// type erased manner.
  543. disconnector _disconnector;
  544. /// Shared pointer to the disconnector. All connection objects has a
  545. /// weak pointer to this pointer for performing disconnections.
  546. std::shared_ptr<detail::disconnector> _shared_disconnector;
  547. };
  548. // Implementation of the disconnect operation of the connection class
  549. inline void connection::disconnect() {
  550. auto ptr = _weak_disconnector.lock();
  551. if( ptr ) {
  552. (*ptr)( _index );
  553. }
  554. _weak_disconnector.reset();
  555. }
  556. /// Signal type that is safe to use in multithreaded environments,
  557. /// where the signal and slots exists in different threads.
  558. /// The multithreaded policy provides mutexes and locks to synchronize
  559. /// access to the signals internals.
  560. ///
  561. /// This is the recommended signal type, even for single threaded
  562. /// environments.
  563. template <class T> using signal = signal_type<multithread_policy, T>;
  564. /// Signal type that is unsafe in multithreaded environments.
  565. /// No synchronizations are provided to the signal_type for accessing
  566. /// the internals.
  567. ///
  568. /// Only use this signal type if you are sure that your environment is
  569. /// single threaded and performance is of importance.
  570. template <class T> using unsafe_signal = signal_type<singlethread_policy, T>;
  571. } // namespace nod
  572. #endif // IG_NOD_INCLUDE_NOD_HPP