lxgui
utils_signal.hpp
1 #ifndef LXGUI_UTILS_SIGNAL_HPP
2 #define LXGUI_UTILS_SIGNAL_HPP
3 
4 #include "lxgui/utils.hpp"
5 #include "lxgui/utils_observer.hpp"
6 #include "lxgui/utils_view.hpp"
7 
8 #include <algorithm>
9 #include <functional>
10 #include <memory>
11 #include <vector>
12 
13 namespace lxgui::utils {
14 
17 namespace signal_impl {
18 
19 struct slot_base {
20  bool disconnected = false;
21 };
22 
23 } // namespace signal_impl
27 class connection {
29  template<typename T>
30  friend class signal;
31 
32  explicit connection(utils::observer_ptr<signal_impl::slot_base> slot) noexcept :
33  slot_(std::move(slot)) {}
34 
35 public:
37  connection() = default;
38 
39  // Copiable, movable.
40  connection(const connection&) = default;
41  connection(connection&&) = default;
42  connection& operator=(const connection&) = default;
44 
46  void disconnect() noexcept {
47  if (auto* raw = slot_.get()) {
48  raw->disconnected = true;
49  slot_ = nullptr;
50  }
51  }
52 
54  [[nodiscard]] bool connected() const noexcept {
55  if (auto* raw = slot_.get())
56  return !raw->disconnected;
57  else
58  return false;
59  }
60 
61 private:
62  utils::observer_ptr<signal_impl::slot_base> slot_;
63 };
64 
66 class scoped_connection : private connection {
67 public:
69  scoped_connection() = default;
70 
72  scoped_connection(connection c) noexcept : connection(std::move(c)) {}
73 
75  ~scoped_connection() noexcept {
76  disconnect();
77  }
78 
79  // Non-copiable, movable.
84 
87 
90 };
91 
141 template<typename T>
142 class signal {
143 public:
151  using function_type = std::function<T>;
152 
153 private:
159  struct slot : signal_impl::slot_base {
160  explicit slot(function_type func) noexcept : callback(std::move(func)) {}
161 
162  function_type callback;
163  };
164 
187  template<typename U>
188  using container_type = std::vector<U>;
189 
197  using slot_list = container_type<utils::owner_ptr<slot>>;
198 
200  template<typename BaseIterator>
201  struct slot_dereferencer {
202  using data_type = const function_type&;
203  static data_type dereference(const BaseIterator& iter) noexcept {
204  return (*iter)->callback;
205  }
206  };
207 
209  template<typename BaseIterator>
210  struct non_disconnected_filter {
211  static bool is_included(const BaseIterator& iter) noexcept {
212  return !(*iter)->disconnected;
213  }
214  };
215 
216 public:
220 
222  signal() : impl_(std::make_shared<implementation>()) {}
223 
226  // Mark all slots as disconnected, in case the destructor
227  // is called midway through the signal being emitted.
228  disconnect_all();
229  }
230 
231  // Non-copiable, movable.
232  signal(const signal&) = delete;
233  signal(signal&&) = default;
234  signal& operator=(const signal&) = delete;
235  signal& operator=(signal&&) = default;
236 
238  void disconnect_all() noexcept {
239  if (impl_->recursion == 0u) {
240  impl_->slots.clear();
241  return;
242  }
243 
244  // We are in the middle of an iteration; just mark slot as
245  // disconnected. The slots will not be called, however they still
246  // occupy a spot in the slot list. This is done to avoid invalidating
247  // iterators while iterating over the slot list, if one of the
248  // slot happens to disconnect itself. The memory will be cleared
249  // up when the signal is no longer being iterated on.
250  for (auto& slt : impl_->slots)
251  slt->disconnected = true;
252  }
253 
258  [[nodiscard]] bool empty() const noexcept {
259  const auto& slots = impl_->slots;
260  return std::none_of(
261  slots.begin(), slots.end(), [](const auto& slt) { return !slt->disconnected; });
262  }
263 
269  [[nodiscard]] slot_list_view slots() const noexcept {
270  return slot_list_view(impl_->slots);
271  }
272 
284  impl_->slots.push_back(utils::make_owned<slot>(std::move(function)));
285  return connection(impl_->slots.back());
286  }
287 
292  template<typename... Args>
293  void operator()(Args&&... args) {
294  // Make a shared-ownership copy of the slot list, so that the list
295  // survives even if this signal is destroyed midway during a slot.
296  // To support this, we must not use any member variable or member
297  // function of the signal object, which may be destroyed at any time.
298  [[maybe_unused]] const auto impl_copy_ptr = impl_;
299  auto& impl = *impl_;
300  const auto& slots = impl.slots;
301  auto& recursion = impl.recursion;
302 
303  ++recursion;
304 
305  // Call the slots
306  // NB: Cache the size here, so new slots connected will not trigger
307  // NB: Use integer-based iteration since iterators may be invalidated on insertion
308  const std::size_t num_slots = slots.size();
309  for (std::size_t i = 0; i < num_slots; ++i) {
310  auto& slt = *slots[i];
311  if (!slt.disconnected)
312  slt.callback(args...);
313  }
314 
315  --recursion;
316 
317  if (recursion == 0u)
318  impl.garbage_collect();
319  }
320 
321 private:
322  struct implementation {
323  slot_list slots;
324  std::size_t recursion = 0u;
325 
326  void garbage_collect() noexcept {
327  auto iter = std::remove_if(
328  slots.begin(), slots.end(), [](const auto& slt) { return slt->disconnected; });
329 
330  slots.erase(iter, slots.end());
331  }
332  };
333 
334  std::shared_ptr<implementation> impl_;
335 };
336 
337 } // namespace lxgui::utils
338 
339 #endif
Object representing the connection between a slot and a signal.
connection()=default
Default constructor, no connection.
bool connected() const noexcept
Check if this slot is still connected.
connection(connection &&)=default
connection(const connection &)=default
connection & operator=(const connection &)=default
void disconnect() noexcept
Disconnect the slot.
connection & operator=(connection &&)=default
A connection that automatically disconnects when going out of scope.
scoped_connection & operator=(const scoped_connection &)=delete
scoped_connection(scoped_connection &&)=default
~scoped_connection() noexcept
Destructor, disconnects.
scoped_connection(connection c) noexcept
Conversion constructor from a raw connection.
scoped_connection(const scoped_connection &)=delete
void disconnect() noexcept
Disconnect the slot.
scoped_connection & operator=(scoped_connection &&)=default
scoped_connection()=default
Default constructor, no connection.
Generic class for observing and triggering events.
slot_list_view slots() const noexcept
Return a constant view onto the connected slots.
utils::view::adaptor< slot_list, slot_dereferencer, non_disconnected_filter > slot_list_view
Type of the view returned by slots().
signal & operator=(signal &&)=default
void disconnect_all() noexcept
Disconnects all slots.
bool empty() const noexcept
Check if this signal contains any slot.
signal & operator=(const signal &)=delete
connection connect(function_type function)
Connect a new slot to this signal.
signal(const signal &)=delete
signal()
Default constructor (no slot).
signal(signal &&)=default
void operator()(Args &&... args)
Trigger the signal.
std::function< T > function_type
Type of the callable function stored in a slot. Can use any function/delegate type here,...
Allow iterating over a container without access to the container itself.
Definition: utils_view.hpp:60