KDBindings API Documentation  1.0.95
signal.h
Go to the documentation of this file.
1 /*
2  This file is part of KDBindings.
3 
4  SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5  Author: Sean Harmer <sean.harmer@kdab.com>
6 
7  SPDX-License-Identifier: MIT
8 
9  Contact KDAB at <info@kdab.com> for commercial licensing options.
10 */
11 
12 #pragma once
13 
14 #include <assert.h>
15 #include <memory>
16 #include <stdexcept>
17 #include <type_traits>
18 #include <utility>
19 #include <forward_list>
20 
21 #ifdef emit
22 static_assert(false, "KDBindings is not compatible with Qt's 'emit' keyword.\n"
23  "To use KDBindings with Qt, please define QT_NO_EMIT to disable Qt's 'emit' keyword.\n"
24  "If you're using CMake you can set KDBindings_QT_NO_EMIT to ON to add this.");
25 
26 // Undefine emit to suppress more compiler errors after the static_assert failure.
27 // Otherwise the compiler would drown our custom error message with more errors.
28 #undef emit
29 #endif
30 
33 #include <kdbindings/utils.h>
34 
36 
42 namespace KDBindings {
70 template<typename... Args>
71 class Signal
72 {
73  static_assert(
74  std::conjunction<std::negation<std::is_rvalue_reference<Args>>...>::value,
75  "R-value references are not allowed as Signal parameters!");
76 
77  // The Signal::Impl class exists, so Signals can be implemented in a PIMPL-like way.
78  // This allows us to easily move Signals without losing their ConnectionHandles, as well as
79  // making an unconnected Signal only sizeof(shared_ptr).
80  class Impl : public Private::SignalImplBase
81  {
82  public:
83  Impl() noexcept { }
84 
85  ~Impl() noexcept { }
86 
87  // Signal::Impls are not copyable
88  Impl(Impl const &other) = delete;
89  Impl &operator=(Impl const &other) = delete;
90 
91  // Signal::Impls are not moveable, this would break the ConnectionHandles
92  Impl(Impl &&other) = delete;
93  Impl &operator=(Impl &&other) = delete;
94 
95  // Connects a std::function to the signal. The returned
96  // value can be used to disconnect the function again.
97  Private::GenerationalIndex connect(std::function<void(Args...)> const &slot)
98  {
99  Connection newConnection;
100  newConnection.slot = slot;
101  return m_connections.insert(std::move(newConnection));
102  }
103 
104  // Establish a deferred connection between signal and slot, where ConnectionEvaluator object
105  // is used to queue all the connection to evaluate later. The returned
106  // value can be used to disconnect the slot later.
107  Private::GenerationalIndex connectDeferred(const std::shared_ptr<ConnectionEvaluator> &evaluator, std::function<void(Args...)> const &slot)
108  {
109  auto weakEvaluator = std::weak_ptr<ConnectionEvaluator>(evaluator);
110 
111  auto deferredSlot = [weakEvaluator = std::move(weakEvaluator), slot](ConnectionHandle &handle, Args... args) {
112  if (auto evaluatorPtr = weakEvaluator.lock()) {
113  auto lambda = [slot, args...]() {
114  slot(args...);
115  };
116  evaluatorPtr->enqueueSlotInvocation(handle, lambda);
117  } else {
118  throw std::runtime_error("ConnectionEvaluator is no longer alive");
119  }
120  };
121 
122  Connection newConnection;
123  newConnection.m_connectionEvaluator = evaluator;
124  newConnection.slotReflective = deferredSlot;
125 
126  return m_connections.insert(std::move(newConnection));
127  }
128 
129  Private::GenerationalIndex connectReflective(std::function<void(ConnectionHandle &handle, Args...)> const &slot)
130  {
131  Connection newConnection;
132  newConnection.slotReflective = slot;
133 
134  return m_connections.insert(std::move(newConnection));
135  }
136 
137  // Disconnects a previously connected function
138  //
139  // WARNING: While this function is marked with noexcept, it *may* terminate the program
140  // if it is not possible to allocate memory or if mutex locking isn't possible.
141  void disconnect(const ConnectionHandle &handle) noexcept override
142  {
143  // If the connection evaluator is still valid, remove any queued up slot invocations
144  // associated with the given handle to prevent them from being evaluated in the future.
145  auto idOpt = handle.m_id; // Retrieve the connection associated with this id
146 
147  // Proceed only if the id is valid
148  if (idOpt.has_value()) {
149  auto id = idOpt.value();
150 
151  // Retrieve the connection associated with this id
152  auto connection = m_connections.get(id);
153  if (connection && m_isEmitting) {
154  // We are currently still emitting the signal, so we need to defer the actual
155  // disconnect until the emit is done.
156  connection->toBeDisconnected = true;
157  m_disconnectedDuringEmit = true;
158  return;
159  }
160 
161  if (connection && connection->m_connectionEvaluator.lock()) {
162  if (auto evaluatorPtr = connection->m_connectionEvaluator.lock()) {
163  evaluatorPtr->dequeueSlotInvocation(handle);
164  }
165  }
166 
167  // Note: This function may throw if we're out of memory.
168  // As `disconnect` is marked as `noexcept`, this will terminate the program.
169  m_connections.erase(id);
170  }
171  }
172 
173  // Disconnects all previously connected functions
174  //
175  // WARNING: While this function is marked with noexcept, it *may* terminate the program
176  // if it is not possible to allocate memory or if mutex locking isn't possible.
177  void disconnectAll() noexcept
178  {
179  const auto numEntries = m_connections.entriesSize();
180 
181  const auto sharedThis = shared_from_this();
182  for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
183  const auto indexOpt = m_connections.indexAtEntry(i);
184  if (sharedThis && indexOpt) {
185  disconnect(ConnectionHandle(sharedThis, *indexOpt));
186  }
187  }
188  }
189 
190  bool blockConnection(const Private::GenerationalIndex &id, bool blocked) override
191  {
192  Connection *connection = m_connections.get(id);
193  if (connection) {
194  const bool wasBlocked = connection->blocked;
195  connection->blocked = blocked;
196  return wasBlocked;
197  } else {
198  throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
199  }
200  }
201 
202  bool isConnectionActive(const Private::GenerationalIndex &id) const noexcept override
203  {
204  return m_connections.get(id);
205  }
206 
207  bool isConnectionBlocked(const Private::GenerationalIndex &id) const override
208  {
209  auto connection = m_connections.get(id);
210  if (connection) {
211  return connection->blocked;
212  } else {
213  throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
214  }
215  }
216 
217  void emit(Args... p)
218  {
219  if (m_isEmitting) {
220  throw std::runtime_error("Signal is already emitting, nested emits are not supported!");
221  }
222  m_isEmitting = true;
223 
224  const auto numEntries = m_connections.entriesSize();
225 
226  // This loop can *not* tolerate new connections being added to the signal inside a slot
227  // Doing so will be undefined behavior
228  for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
229  const auto index = m_connections.indexAtEntry(i);
230 
231  if (index) {
232  const auto con = m_connections.get(*index);
233 
234  if (!con->blocked) {
235  if (con->slotReflective) {
236  if (auto sharedThis = shared_from_this(); sharedThis) {
237  ConnectionHandle handle(sharedThis, *index);
238  con->slotReflective(handle, p...);
239  }
240  } else if (con->slot) {
241  con->slot(p...);
242  }
243  }
244  }
245  }
246  m_isEmitting = false;
247 
248  if (m_disconnectedDuringEmit) {
249  m_disconnectedDuringEmit = false;
250 
251  // Because m_connections is using a GenerationIndexArray, this loop can tolerate
252  // deletions inside the loop. So iterating over the array and deleting entries from it
253  // should not lead to undefined behavior.
254  for (auto i = decltype(numEntries){ 0 }; i < numEntries; ++i) {
255  const auto index = m_connections.indexAtEntry(i);
256 
257  if (index.has_value()) {
258  const auto con = m_connections.get(index.value());
259  if (con->toBeDisconnected) {
260  disconnect(ConnectionHandle(shared_from_this(), index));
261  }
262  }
263  }
264  }
265  }
266 
267  private:
268  friend class Signal;
269  struct Connection {
270  std::function<void(Args...)> slot;
271  std::function<void(ConnectionHandle &, Args...)> slotReflective;
272  std::weak_ptr<ConnectionEvaluator> m_connectionEvaluator;
273  bool blocked{ false };
274  // When we disconnect while the signal is still emitting, we need to defer the actual disconnection
275  // until the emit is done. This flag is set to true when the connection should be disconnected.
276  bool toBeDisconnected{ false };
277  };
278 
279  mutable Private::GenerationalIndexArray<Connection> m_connections;
280 
281  // If a reflective slot disconnects itself, we need to make sure to not deconstruct the std::function
282  // while it is still running.
283  // Therefore, defer all slot disconnections until the emit is done.
284  //
285  // Previously, we stored the ConnectionHandles that were to be disconnected in a list.
286  // However, that would mean that disconnecting cannot be noexcept, as it may need to allocate memory in that list.
287  // We can fix this by storing this information within each connection itself (the toBeDisconnected flag).
288  // Because of the memory layout of the `struct Connection`, this shouldn't even use any more memory.
289  //
290  // The only downside is that we need to iterate over all connections to find the ones that need to be disconnected.
291  // This is helped by using the m_disconnedDuringEmit flag to avoid unnecessary iterations.
292  bool m_isEmitting = false;
293  bool m_disconnectedDuringEmit = false;
294  };
295 
296 public:
298  Signal() = default;
299 
303  Signal(const Signal &) = delete;
304  Signal &operator=(Signal const &other) = delete;
305 
307  Signal(Signal &&other) noexcept = default;
308  Signal &operator=(Signal &&other) noexcept = default;
309 
319  ~Signal() noexcept
320  {
321  disconnectAll();
322  }
323 
337  KDBINDINGS_WARN_UNUSED ConnectionHandle connect(std::function<void(Args...)> const &slot)
338  {
339  ensureImpl();
340 
341  return ConnectionHandle{ m_impl, m_impl->connect(slot) };
342  }
343 
358  KDBINDINGS_WARN_UNUSED ConnectionHandle connectReflective(std::function<void(ConnectionHandle &, Args...)> const &slot)
359  {
360  ensureImpl();
361 
362  return ConnectionHandle{ m_impl, m_impl->connectReflective(slot) };
363  }
364 
377  KDBINDINGS_WARN_UNUSED ConnectionHandle connectSingleShot(std::function<void(Args...)> const &slot)
378  {
379  return connectReflective([slot](ConnectionHandle &handle, Args... args) {
380  handle.disconnect();
381  slot(args...);
382  });
383  }
384 
407  KDBINDINGS_WARN_UNUSED ConnectionHandle connectDeferred(const std::shared_ptr<ConnectionEvaluator> &evaluator, std::function<void(Args...)> const &slot)
408  {
409  ensureImpl();
410 
411  ConnectionHandle handle(m_impl, {});
412  handle.setId(m_impl->connectDeferred(evaluator, slot));
413  return handle;
414  }
415 
450  // The enable_if_t makes sure that this connect function specialization is only
451  // available if we provide a function that cannot be otherwise converted to a
452  // std::function<void(Args...)>, as it otherwise tries to take precedence
453  // over the normal connect function.
454  template<typename Func, typename... FuncArgs, typename = std::enable_if_t<std::disjunction_v<std::negation<std::is_convertible<Func, std::function<void(Args...)>>>, std::integral_constant<bool, sizeof...(FuncArgs) /*Also enable this function if we want to bind at least one argument*/>>>>
455  KDBINDINGS_WARN_UNUSED ConnectionHandle connect(Func &&slot, FuncArgs &&...args)
456  {
457  std::function<void(Args...)> bound = Private::bind_first(std::forward<Func>(slot), std::forward<FuncArgs>(args)...);
458  return connect(bound);
459  }
460 
470  void disconnect(const ConnectionHandle &handle)
471  {
472  if (m_impl && handle.belongsTo(*this) && handle.m_id.has_value()) {
473  m_impl->disconnect(handle);
474  // TODO check if Impl is now empty and reset
475  } else {
476  throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
477  }
478  }
479 
489  void disconnectAll() noexcept
490  {
491  if (m_impl) {
492  m_impl->disconnectAll();
493  // Once all connections are disconnected, we can release ownership of the Impl.
494  // This does not destroy the Signal itself, just the Impl object.
495  // If another slot is connected, another Impl object will be constructed.
496  m_impl.reset();
497  }
498  // If m_impl is nullptr, we don't have any connections to disconnect
499  }
500 
518  bool blockConnection(const ConnectionHandle &handle, bool blocked)
519  {
520  if (m_impl && handle.belongsTo(*this) && handle.m_id.has_value()) {
521  return m_impl->blockConnection(*handle.m_id, blocked);
522  } else {
523  throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
524  }
525  }
526 
536  bool isConnectionBlocked(const ConnectionHandle &handle) const
537  {
538  assert(handle.belongsTo(*this));
539  if (!m_impl) {
540  throw std::out_of_range("Provided ConnectionHandle does not match any connection\nLikely the connection was deleted before!");
541  }
542 
543  if (handle.m_id.has_value()) {
544  return m_impl->isConnectionBlocked(*handle.m_id);
545  } else {
546  return false;
547  }
548  }
549 
568  void emit(Args... p) const
569  {
570  if (m_impl)
571  m_impl->emit(p...);
572 
573  // if m_impl is nullptr, we don't have any slots connected, don't bother emitting
574  }
575 
576 private:
577  friend class ConnectionHandle;
578 
579  void ensureImpl()
580  {
581  if (!m_impl) {
582  m_impl = std::make_shared<Impl>();
583  }
584  }
585 
586  // shared_ptr is used here instead of unique_ptr, so ConnectionHandle instances can
587  // use a weak_ptr to check if the Signal::Impl they reference is still alive.
588  //
589  // This makes Signals easily copyable in theory, but the semantics of this are unclear.
590  // Copying could either simply copy the shared_ptr, which means the copy would share
591  // the connections of the original, which is possibly unintuitive, or the Impl would
592  // have to be copied as well.
593  // This would however leave connections without handles to disconnect them.
594  // So copying is forbidden for now.
595  //
596  // Think of this shared_ptr more like a unique_ptr with additional weak_ptr's
597  // in ConnectionHandle that can check whether the Impl object is still alive.
598  mutable std::shared_ptr<Impl> m_impl;
599 };
600 
613 {
614 public:
621  explicit ConnectionBlocker(const ConnectionHandle &handle)
622  : m_handle{ handle }
623  {
624  m_wasBlocked = m_handle.block(true);
625  }
626 
632  {
633  m_handle.block(m_wasBlocked);
634  }
635 
636 private:
637  ConnectionHandle m_handle;
638  bool m_wasBlocked{ false };
639 };
640 
702 } // namespace KDBindings
#define KDBINDINGS_WARN_UNUSED
A ConnectionBlocker is a convenient RAII-style mechanism for temporarily blocking a connection.
Definition: signal.h:613
ConnectionBlocker(const ConnectionHandle &handle)
Definition: signal.h:621
A ConnectionHandle represents the connection of a Signal to a slot (i.e. a function that is called wh...
bool belongsTo(const Signal< Args... > &signal) const
A Signal provides a mechanism for communication between objects.
Definition: signal.h:72
~Signal() noexcept
Definition: signal.h:319
Signal(Signal &&other) noexcept=default
bool isConnectionBlocked(const ConnectionHandle &handle) const
Definition: signal.h:536
KDBINDINGS_WARN_UNUSED ConnectionHandle connectDeferred(const std::shared_ptr< ConnectionEvaluator > &evaluator, std::function< void(Args...)> const &slot)
Establishes a deferred connection between the provided evaluator and slot.
Definition: signal.h:407
void disconnect(const ConnectionHandle &handle)
Definition: signal.h:470
bool blockConnection(const ConnectionHandle &handle, bool blocked)
Definition: signal.h:518
Signal & operator=(Signal &&other) noexcept=default
void disconnectAll() noexcept
Definition: signal.h:489
KDBINDINGS_WARN_UNUSED ConnectionHandle connectReflective(std::function< void(ConnectionHandle &, Args...)> const &slot)
Definition: signal.h:358
void emit(Args... p) const
Definition: signal.h:568
Signal(const Signal &)=delete
KDBINDINGS_WARN_UNUSED ConnectionHandle connect(Func &&slot, FuncArgs &&...args)
Definition: signal.h:455
KDBINDINGS_WARN_UNUSED ConnectionHandle connect(std::function< void(Args...)> const &slot)
Definition: signal.h:337
Signal & operator=(Signal const &other)=delete
KDBINDINGS_WARN_UNUSED ConnectionHandle connectSingleShot(std::function< void(Args...)> const &slot)
Definition: signal.h:377
friend class ConnectionHandle
Definition: signal.h:577
The main namespace of the KDBindings library.
Definition: binding.h:21

© Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
KDBindings
Reactive programming & data binding in C++
https://github.com/KDAB/KDBindings/
Generated by doxygen 1.9.1