Featured image of post Signals and Slots

Signals and Slots

Abstract

Dopo essere entrato in contatto con il framework Qt, mi sono sempre chiesto come implementare un meccanismo di signal/slot nell’era del moderno C++.

Wikipedia ci fornisce la sottostante definizione.

[…] a language construct […] which makes it easy to implement the Observer pattern while avoiding boilerplate code. The concept is that GUI widgets can send signals containing event information which can be received by other controls using special functions known as slots.

– Wikipedia

Tradotto in un lessico più masticabile, il meccanismo di signal/slots consente la comunicazione inter-oggetto basata sugli eventi. Esistono svariate librerie che implementanto questo pattern di programmazione in maniera eccellente, tuttavia oggi vi fornirò la mia modesta implementazione.

Diagramma UML

Di seguito un semplicissimo diagramma UML che spiega sommariamente la struttura del progetto.

Codice

Essendo una classe template, tutto il codice viene riposto in un header file, per ovvi motivi. Tutto il codice è interamente commentato per rendere più chiara la lettura.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
#ifndef CPP_PROJECT_STRUCTURE_LIBSIGSLOTPP_INCLUDE_LIBSIGSLOTPP_SIGNAL_HPP_
#define CPP_PROJECT_STRUCTURE_LIBSIGSLOTPP_INCLUDE_LIBSIGSLOTPP_SIGNAL_HPP_

#include <functional>
#include <memory>

namespace sigslotpp {

typedef std::size_t IDType;

/**
 * @brief Interface ISlotConnection
 * It provides a simple interface that identifies the Slot and its main
 * connectivity operations.
 * ISignal can access private and protected regions dor design purposes.
 */
class ISlotConnection {
 private:
  friend class ISignal;

  IDType id_;         ///< identifier and index into the signal slots vector.
  bool isConnected_;  ///< slot connection status.
  bool isBlocked_;    ///< slot connection block status.

 protected:
  /**
   * @brief Getter for the identifier
   * @return id_
   */
  IDType id() const noexcept { return id_; }

  /**
   * @brief Getter/Setter for identifier identifier
   * @return a reference to id_
   */
  IDType &id() noexcept { return id_; }

  /**
   * @brief Clears the slot connection
   * Where the real disconnection happens.
   * This gets called by the ISlotConnection::disconnect() function, the
   * implementation depends by the child subclasses.
   */
  virtual void clear() noexcept = 0;

 public:
  ISlotConnection() = delete;  // not needed but better for compilation error

  /**
   * @brief Primary constructor
   * @post id_ == id
   * @post isConnected__ == isConnected
   * @post iSBlocked_ == isBlocked
   */
  explicit ISlotConnection(const IDType id, const bool isConnected = true,
                           const bool isBlocked = false) noexcept
      : id_(id), isConnected_(isConnected), isBlocked_(isBlocked) {}

  /**
   * @brief Destructor
   */
  virtual ~ISlotConnection() {}

 public:
  /**
   * @brief Checks the connection status
   * Used to see if a slot is still connected to a Signal
   */
  virtual bool isConnected() const noexcept { return isConnected_; }

  /**
   * @brief Checks if the connection is blocked
   * Used to temporarily disable slot invocation.
   * @return true if the slot invocation is blocked else false.
   */
  bool isBlocked() const noexcept { return isBlocked_; }

 public:
  /**
   * @brief Blocks the slot invocation
   * @post isBlocked_ == true
   */
  void block() noexcept { isBlocked_ = true; }

  /**
   * @brief Unblocks the slot invocation
   * @post isBlocked_ == false
   */
  void unblock() noexcept { isBlocked_ = false; }

  /**
   * @brief Disconnects the slot from the signal
   * @post isConnected_ == false
   */
  void disconnect() {
    if (isConnected_) {
      isConnected_ = false;
      clear();
    }
  }
};

/**
 * @brief Class Connection
 * This class allows interaction with an ongoing signal-slot connection and
 * exposes an interface to manipulate and query the status of this connection.
 * Note that Connection is not a RAII object, one does not need to hold one
 * such object to keep the signal-slot connection alive.
 */
class Connection {
 private:
  std::weak_ptr<ISlotConnection> slot_;  ///< the slot to manipulate and query

 public:
  Connection() = delete;  // not needed but better for compilation error

  /**
   * @brief Primary constructor
   * @param slot that will be handled by this connection
   */
  explicit Connection(std::weak_ptr<ISlotConnection> &&slot) : slot_(slot) {}

  /**
   * @brief Destructor
   */
  virtual ~Connection() {}

 public:
  /**
   * @brief Checks if this connection is still valid
   * To have this information just see if the std::weak_ptr is expired
   * @return true if the connection is valid else false
   */
  bool isValid() const noexcept { return !slot_.expired(); }

  /**
   * @brief Checks if the slot is still connected to its signal
   * @return true if the slot is still connected else false
   * @see ISlotConnection::isConnected()
   */
  bool isConnected() const noexcept {
    std::shared_ptr<ISlotConnection> d = slot_.lock();
    return d && d->isConnected();
  }

  /**
   * @brief Checks if the slot connection is blocked
   * @return true if the slot connection is blocked else false
   * @see ISlotConnection::isBLocked()
   */
  bool isBlocked() const noexcept {
    std::shared_ptr<ISlotConnection> d = slot_.lock();
    return d && d->isBlocked();
  }

  /**
   * @brief Blocks the slot invocation
   * @see ISlotConnection::block()
   */
  void block() noexcept {
    std::shared_ptr<ISlotConnection> d = slot_.lock();
    if (d) d->block();
  }

  /**
   * @brief Unblocks the slot invocation
   * @see ISlotConnection::unblock()
   */
  void unblock() noexcept {
    std::shared_ptr<ISlotConnection> d = slot_.lock();
    if (d) d->unblock();
  }

  /**
   * @brief Disconnects the slot from its signal
   * @see ISlotConnection::disconnect()
   */
  void disconnect() {
    std::shared_ptr<ISlotConnection> d = slot_.lock();
    if (d) d->disconnect();
  }
};

///////////////////////////////

/**
 * @brief Interface ISignal
 * It provides a simple interface that identifies the Signal and its essential
 * functions.
 */
class ISignal {
 protected:
  /**
   * @brief Getter for specified ISlotConnection identifier
   * @return id of the specified slot
   */
  IDType idOf(ISlotConnection *const slotPtr) const noexcept {
    return slotPtr->id();
  }

  /**
   * @brief Getter/Setter for specified ISlotConnection identifier
   * @return a reference to id of the specified slot
   */
  IDType &idOf(ISlotConnection *const slotPtr) noexcept {
    return slotPtr->id();
  }

 public:
  /**
   * @brief Destructor
   */
  virtual ~ISignal() {}

  /**
   * @brief Disconnect the slot from this signal
   * @param slot a pointer to the slot to disconnect from
   */
  virtual void disconnect(ISlotConnection *const slot) = 0;
};

/**
 * @brief Interface ISlot
 * It provides a simple interface that identifies the Slot and its core
 * invocation functionality.
 * @tparam Args... the argument types of the function to call
 */
template <typename... Args>
class ISlot : public ISlotConnection {
 private:
  ISignal &signal_;  ///< reference to the signal

 protected:
  /**
   * @brief Clears the slot connection
   */
  virtual void clear() noexcept override { signal_.disconnect(this); }

  /**
   * @brief Invokes the function
   * Where the function is really invoked.
   * This gets called by the ISlot::operator()() function, the
   * implementation depends by the child subclasses.
   * @param args the arguments of the function to call
   */
  virtual void invoke(Args... args) = 0;

 public:
  ISlot() = delete;  // not needed but better for compilation error

  /**
   * @brief Primary constructor
   * We have a reference that must be initialized, so no default constructor
   * allowed here.
   * @param id the slot identifier
   * @param signal the reference of the signal connected to this slot
   * @post signal_ points to the signal connected to this slot
   * @see ISlotConnection
   */
  ISlot(const IDType id, ISignal &signal) noexcept
      : ISlotConnection(id), signal_(signal) {}

  /**
   * @brief Invokes the function
   * Invokes or calls the function stored in the slot.
   * Take note that i add an extra template here for flexibility and design
   * purposes. As mentioned in Signal::emit() we use the signature here.
   * At the end it is used std::forward to forward lvalues as either lvalues
   * or as rvalues, depending on Params.
   * @see Signal::emit()
   * @param params the parameters of the function to call
   * @tparam ...Params types of the parameters of the function to call
   */
  template <typename... Params>
  void operator()(Params &&...params) {
    if (ISlotConnection::isConnected() && !ISlotConnection::isBlocked())
      invoke(std::forward<Params>(params)...);
  }
};

/**
 * @brief Class SimpleSlot
 * Basic slot that does not track anything an thus it is imperative that what
 * is called by the std::function must exceeds the lifetime of signal this
 * slot is connected to.
 * @tparam Args... the argument types of the function to call
 */
template <typename... Args>
class SimpleSlot final : public ISlot<Args...> {
 private:
  std::function<void(Args...)>
      function_;  ///< function to be invoked by this slot

 protected:
  /**
   * @copydoc ISlot::invoke()
   */
  void invoke(Args... args) override final {
    function_(std::forward<Args>(args)...);
  }

 public:
  SimpleSlot() = delete;  // not needed but better for compilation error

  /**
   * @brief Primary constructor
   * @param id the slot identifier
   * @param function to be invoked by this slot
   * @param signal the reference to the signal this slot is connected to
   * @see ISlot
   */
  SimpleSlot(const IDType id, std::function<void(Args...)> &&function,
             ISignal &signal) noexcept
      : ISlot<Args...>(id, signal),
        function_(std::forward<std::function<void(Args...)>>(function)) {}
};

/**
 * @brief Class TrackingSlot
 * Tracking slot that tracks an object with a std::weak_ptr, it is auto
 * disconnected upon expiration of the std::weak_ptr thus no need to take care
 * of complex object lifetime managment.
 * @tparam T the type of the object to track
 * @tparam Args... the argument types of the function to call
 */
template <typename T, typename... Args>
class TrackingSlot final : public ISlot<Args...> {
 private:
  std::function<void(Args...)>
      function_;  ///< function to be invoked by this slot
  std::weak_ptr<T>
      objectPtr_;  ///< the pointer to the object this slot is tracking

 protected:
  /**
   * @copydoc ISlot::invoke()
   */
  void invoke(Args... args) override final {
    // this is probably useless in signle thread
    auto sp = objectPtr_.lock();
    if (!sp) {
      ISlotConnection::disconnect();
      return;
    }
    if (ISlotConnection::isConnected()) {
      function_(args...);
    }
  }

 public:
  TrackingSlot() = delete;  // not needed but better for compilation error

  /**
   * @brief Primary constructor
   * @param id the slot identifier
   * @param objectPtr the pointer to the object to track
   * @param function to be invoked by this slot
   * @param signal the reference to the signal this slot is connected to
   * @see ISlot
   */
  TrackingSlot(const IDType id, std::weak_ptr<T> &&objectPtr,
               std::function<void(Args...)> &&function,
               ISignal &signal) noexcept
      : ISlot<Args...>(id, signal),
        function_(std::forward<std::function<void(Args...)>>(function)),
        objectPtr_(std::forward<std::weak_ptr<T>>(objectPtr)) {}

 public:
  /**
   * @brief Checks if the connection still there
   */
  bool isConnected() const noexcept override final {
    return !objectPtr_.expired() && ISlotConnection::isConnected();
  }
};

/**
 * @brief Class Signal
 * This class manages a collection of ISlots
 * @tparam R the return type of the function to call
 * @tparam Args... the argument types of the function to call
 */
template <typename R, typename... Args>
class Signal final : public ISignal {
 private:
  std::vector<std::shared_ptr<ISlot<Args...>>>
      slots_;  ///< slots connected to this Signal

  /**
   * @brief Disconnects the specified slot
   * @param slot the pointer to the slot to disconnect from
   * @post slots_.size() decremented by one
   */
  void disconnect(ISlotConnection *const slot) noexcept override final {
    IDType index = idOf(slot);
    if (!slots_.empty()) {
      std::swap(slots_[index], slots_.back());
      idOf(slots_[index].get()) = index;
      slots_.pop_back();
    }
  }

 public:
  /**
   * @brief Default constructor
   * @post slots_.size() == 0
   */
  Signal() noexcept : slots_() {}

  /**
   * @brief Destructor
   */
  virtual ~Signal() noexcept { disconnectAll(); }

  /**
   * @brief Move constructor
   * @see Signal::operator=()
   */
  Signal(Signal &&other) noexcept : slots_() { *this = std::move(other); }

  /**
   * @brief Move assignment operator
   * @return a reference to this
   * @post slots_ == other.slots_
   */
  Signal &operator=(Signal &&other) noexcept {
    if (this != &other) {
      slots_ = std::move(other.slots_);
    }
    return *this;
  }

  /**
   * @brief Connects a slot to the signal
   * Connects a std::function to the signal.
   * @param function the signal connects to
   * @return a Connection instance for managment purposes
   * @post slots_.size() incremented by one
   */
  Connection connect(std::function<R(Args...)> &&function) noexcept {
    IDType id = slots_.size();
    std::shared_ptr<ISlot<Args...>> slot =
        std::make_shared<SimpleSlot<Args...>>(
            id, std::forward<std::function<R(Args...)>>(function), *this);
    slots_.push_back(slot);
    return Connection(std::weak_ptr(slots_[id]));
  }

  /**
   * @brief Connects and tracks a slot to the signal
   * Connects a std::function to the signal but this time the slot is tracked
   * by a std::weak_ptr pointing to the object of type T. The purpose is to
   * disconnect this slot automatically upon said object destruction.
   * @param function the signal connects to
   * @param objectPtr pointer to the object to be tracked
   * @tparam T the type of the object to track
   * @return a Connection instance for managment purposes
   * @post slots_.size() incremented by one
   */
  template <typename T>
  Connection connect(std::weak_ptr<T> &&objectPtr,
                     std::function<R(Args...)> &&function) noexcept {
    IDType id = slots_.size();
    std::shared_ptr<ISlot<Args...>> slot =
        std::make_shared<TrackingSlot<T, Args...>>(
            id, std::forward<std::weak_ptr<T>>(objectPtr),
            std::forward<std::function<R(Args...)>>(function), *this);
    slots_.push_back(slot);
    return Connection(std::weak_ptr(slots_[id]));
  }

  /**
   * @brief Connects and tracks a slot to the signal
   * Convenience method to connect a member function.
   * Connects a function pointer to the signal.
   * The slot is tracked by a std::weak_ptr pointing to the object of type T.
   * The purpose is to disconnect this slot automatically upon said object
   * destruction.
   * @param function pointer the signal connects to
   * @param objectPtr pointer to the object to be tracked
   * @tparam T the type of the object to track
   * @return a Connection instance for managment purposes
   * @post slots_.size() incremented by one
   */
  template <typename T>
  Connection connect(std::shared_ptr<T> &objectPtr,
                     R (T::*function)(Args...)) noexcept {
    T *const ptr = objectPtr.get();
    return connect(std::weak_ptr<T>(objectPtr),
                   [ptr, function](Args... args) -> R {
                     return (ptr->*function)(args...);
                   });
  }

  /**
   * @brief Connects and tracks a slot to the signal
   * Convenience method to connect a const member function.
   * Connects a function pointer to the signal.
   * The slot is tracked by a std::weak_ptr pointing to the object of type T.
   * The purpose is to disconnect this slot automatically upon said object
   * destruction.
   * @param function pointer the signal connects to
   * @param objectPtr pointer to the object to be tracked
   * @tparam T the type of the object to track
   * @return a Connection instance for managment purposes
   * @post slots_.size() incremented by one
   */
  template <typename T>
  Connection connect(std::shared_ptr<T> &objectPtr,
                     R (T::*function)(Args...) const) noexcept {
    T *const ptr = objectPtr.get();
    return connect(std::weak_ptr<T>(objectPtr),
                   [ptr, function](Args... args) -> R {
                     return (ptr->*function)(args...);
                   });
  }

  /**
   * @brief Disconnects all the slots
   * Disconnects all the slots this signal is connected to.
   * @post slots_.empty() == true
   */
  void disconnectAll() noexcept { slots_.clear(); }

  /**
   * @brief Emits the signal
   * All non blocked and connected slot functions will be called
   * with supplied arguments.
   * Important explanation!
   * A template is used here, this is for flexibility and design purposes.
   * To this function can be passed rvalues or lvalues but if we use the Args
   * type it is basically precluding us to emit the signal when it is a
   * different types from Args. Suppose Signal<bool, int, double&> the matched
   * function is std::function<bool(int, double&)>. Now i can't emit something
   * like emit(5, 5.0) because 5.0 is a non const lvalue.
   * @see ISlot::operator()()
   * @param params arguments to emit
   * @tparam ...Params type of the parameter to emit
   */
  template <typename... Params>
  void emit(Params... params) {
    for (auto const &it : slots_) {
      it->operator()(params...);
    }
  }

  /**
   * @brief Gets the number of connected slots
   * @return slots.size()
   */
  std::size_t connectedSlots() const noexcept { return slots_.size(); }
};

}  // namespace sigslotpp

#endif  // CPP_PROJECT_STRUCTURE_LIBSIGSLOTPP_INCLUDE_LIBSIGSLOTPP_SIGNAL_HPP_

Utilizzo

Per comprendere come utilizzare la soluzione implementata vi mostro gli unit tests che ho scritto.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
const std::string ff = "free function";
const std::string mf = "member function";
const std::string smf = "static member function";
const std::string mo = "member operator";
const std::string l = "lambda";
const std::string gl = "generic lambda";

void f() { fmt::print("{}\n", ff); }

struct s {
  void m() { fmt::print("{}\n", mf); }
  static void sm() { fmt::print("{}\n", smf); }
};

struct o {
  void operator()() { fmt::print("{}\n, mo"); }
};

TEST_CASE("slots can be added and removed from the signal") {
  std::shared_ptr<s> d;
  auto lambda = []() { fmt::print("{}\n", l); };
  auto gen_lambda = [](auto &&...a) { fmt::print("{}\n", gl); };

  sigslotpp::Signal<void> sig;

  SUBCASE("connecting slots to signal increases connected slots") {
    auto c1 = sig.connect(f);
    sig.connect(d, &s::m);
    sig.connect(&s::sm);
    auto c2 = sig.connect(o());
    sig.connect(lambda);
    sig.connect(gen_lambda);
    CHECK(sig.connectedSlots() == 6);
  }

  SUBCASE("discconnecting all slots from the signal put size to zero") {
    auto c1 = sig.connect(f);
    auto c2 = sig.connect(o());
    sig.connect(lambda);
    sig.connect(gen_lambda);
    sig.disconnectAll();
    CHECK(sig.connectedSlots() == 0);
  }

  SUBCASE("disconnecting slots from signal decreases connected slots") {
    auto c1 = sig.connect(f);
    auto c2 = sig.connect(o());
    c1.disconnect();
    CHECK(sig.connectedSlots() == 1);
  }
}

TEST_CASE("signal can be emitted") {
  int x{0};
  auto lambda = [&x](int value) { x = x + value; };

  sigslotpp::Signal<void, int> sig;

  sig.connect(lambda);

  sig.emit(5);
  CHECK(x == 5);
  sig.emit(1);
  CHECK(x == 6);
  sig.emit(-6);
  CHECK(x == 0);
}

int sum = 0;
struct x {
  void f(int i) { sum += i; }
};

TEST_CASE("signal can be automatically disconnected") {
  auto a = std::make_shared<x>();
  sigslotpp::Signal<void, int> sig;
  sig.connect(a, &x::f);
  sig.emit(4);
  sig.emit(3);
  sig.emit(-2);
  CHECK(sum == 5);
  a.reset();
  sig.emit(9);
  CHECK(sum == 5);
  CHECK(sig.connectedSlots() == 0);
}

TEST_CASE("signal connection to slot can be blocked") {
  int x{0};
  auto lambda = [&x](int value) { x = x + value; };

  sigslotpp::Signal<void, int> sig;

  auto c1 = sig.connect(lambda);

  sig.emit(5);
  CHECK(x == 5);
  c1.block();
  sig.emit(1);
  CHECK(x == 5);
  c1.unblock();
  sig.emit(-6);
  CHECK(x == -1);
}

Analisi

In questione abbiamo le seguenti classi:

  • ISlotConnection
  • ISlot
  • SimpleSlot
  • TrackingSLot
  • ISignal
  • Signal
  • Connection

Non sento il bisogno di analizzare tutte le classi vista la chiarezza del codice, tuttavia ve ne sono due che meritano una spiegazione accurata ossia TrackingSlot e Connection.

Classe TrackingSlot

Questa classe permette tramite un std::weak_ptr di poter tracciare l’esistenza in memoria di un oggetto specificato. Per farlo viene richiesto da un punto di vista progettuale l’utilizzo degli smart pointers in particolare di un std::shared_ptr per istanziare l’oggetto da tracciare.

Quando il weak_ptr non riesce più ad effettuare il lock significa che l’oggetto tracciato ha terminato il suo ciclo di vita.

Questo slot non verrà rimosso in seguito alla fine del ciclo di vita del suddetto oggetto, ma alla prossima richiesta di un emit del signal a cui è connesso.

È doveroso specificare che questa classe non interagisce sul ciclo di vita dell’oggetto tracciato.

Classe Connection

Questa classe non segue il RAII pattern, ma poco importa. Serve per avere un punto di accesso, l’unico che ho voluto implementare, alla connessione tra signal e slot. Connection mantiene un riferimento, un weak_ptr, allo slot creato in seguito ad un signal.connect(...).

La classe può operare sulle seguenti funzioni dello slot:

  • Disconnessione
  • Blocco/Sblocco della ricezione di un emissione di signal
  • Verifica dello stato dello slot

È doveroso specificare che il ciclo di vita di questa classe non intacca il ciclo di vita dello slot.

Problemi aperti

Vi sono nell’immediato alcune lacune dal punto di vista funzionale, poichè:

  1. Non è thread safe.
  2. Non posso disconnetermi direttamente dallo slot (si supponga di avere un segnale che deve essere emesso una sola volta).
  3. Overloading dei metodi.
  4. Metodi con valori default.
  5. Non c’è modo di recuperare il valore di ritorno degli slot.

Ora, il punto 1 non lo tengo in considerazione data la complessità della gestione.

Il punto 4 purtroppo non ho idea di come risolverlo.

Il problema 5 nasce solo per motivi progettuali, ho preferito non implementarlo perchè superfluo per lo scopo della classe Signal. Nella mia implementazione è possibile connetersi a slot con valore di ritorno, che vengono poi trasformati in slot con ritorno void. Per poter implementare il valore di ritorno servirebbe un vettore di appoggio nel quale salvare il ritorno di ogni slot e mettere a disposizione dei metodi che permettano di leggere tale vettore. Ovviamente ogni emit sovrascriverà il suddetto vettore.

Problema: disconnesione diretta dallo slot

Questo problema, grazie alla classe Connection, è praticamente di immediata soluzione. Basta aggiungere una classe ExtendedSlot che abbia come membro una variabile di tipo Connection, cosi da poterla iniettare all’interno della funzione invoke. Tuttavia c’è un compromesso, ovvero dovremmo disporre di funzioni che abbiano come parametro un tipo Connection.

Vediamo come farlo molto velocemente.

Aggiungiamo una classe ExtendedSLot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename... Args>
class ExtendedSlot final : public ISlot<Args...> {
 private:
  std::function<void(Args...)>
      function_;  ///< function to be invoked by this slot
Connection connection_ ///< what we need

 protected:

  void invoke(Args... args) override final {
    function_(connection_, std::forward<Args>(args)...);
  }

 public:

  ExtendedSlot(const IDType id, std::function<void(Args...)> &&function,
             ISignal &signal) noexcept
      : ISlot<Args...>(id, signal),
        function_(std::forward<std::function<void(Args...)>>(function)) {}

  // not the best but does the job for this implementation.
  void setConnection(Connection connection) { connection_ = connection }

};

Modifichiamo Signal e aggiungiamo un metodo connectExtended.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 Connection connectExtended(std::function<R(Args...)> &&function) noexcept {
  IDType id = slots_.size();
  std::shared_ptr<ISlot<Args...>> slot =
      std::make_shared<ExtendedSlot<Args...>>(
          id, std::forward<std::function<R(Args...)>>(function), *this);
  slots_.push_back(slot);
  auto c = Connection(std::weak_ptr(slots_[id]));
  slots_[id]->setConnection(c);
  return c;
}

E per utilizzarlo avremo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main() {
  int i = 0;
  sigslot::signal<void> sig;

  auto f = [](auto &con) {
    i += 1;
    con.disconnect();
  };

  sig.connectExtended(f);
}

Rimane il fastidio di dover aggiungere sempre quel parametro di tipo Connection ad ogni nostro slot e dovrà anche essere il primo nella lista.

Problema: overloading

Se si aggiungessero le seguenti funzioni di supporto risolveremo anche questo.

1
2
3
4
5
6
7
8
9
template <typename T, typename R, typename... Args>
constexpr auto overload(R(I::*ptr)(Args...)) {
    return ptr;
}

template <typename R, typename... Args>
constexpr auto overload(R(*ptr)(Args...)) {
    return ptr;
}

Il parameter pack permette, una volta espanso, di identificare univocamente il metodo corretto. Abbiamo dunque.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct obj {
  void operator()(int) const {}
  void operator()() {}
};

struct foo {
  void bar(int) {}
  void bar() {}

  static void baz(int) {}
  static void baz() {}
};

void moo(int) {}
void moo() {}

int main() {
  sigslot::signal<void, int> sig;

  foo ff;
  sig.connect(overload<int>(&foo::bar), &ff);
  sig.connect(overload<int>(&foo::baz));
  sig.connect(overload<int>(&moo));
  sig.connect(obj());

  sig(0);

  return 0;
}

Conclusione

Abbiamo visto come implementare un meccanismo di signal/slot. La soluzione fornita presenta ovviamente delle pecche, tuttavia fornisce un’interfaccia di utilizzo molto semplice e priva di “magie”.

Con questa classe potremo ora implementare in maniera chiara e pulita l’observer pattern.

A questo link trovate la cartella di progetto che potrete compilare con CMake.

Licensed under CC BY-NC-SA 4.0
Realizzato con Hugo
Tema Stack realizzato da Jimmy