Newsgroup: it.comp.lang.c++
Titolo: [LUNGO] Un approccio al problema dei "segnali" (o eventi)

Salve a tutti.
Premessa: il seguente post è piuttosto lungo e richiede un po' di
attenzione, quindi chi non avesse 5 minuti da dedicargli può tranquillamente
fare a meno di leggerlo :-) ... per chi invece si volesse prendere la briga
di farlo (e spero che ce ne sia almeno uno!), mi piacerebbe molto sentire
cosa ne pensa.
Ho provato a costruire una struttura template generica dn::slot_t che mi
permetta di risolvere nel modo più "indolore" possibile la questione delle
segnalazioni tra classi, nel senso di minimizzare il codice ripetitivo che
dovrà essere inserito "a mano" ... di seguito spiego il funzionamento di
questo tipo di soluzione al problema così come lo ho affrontato io.
Desidererei avere pareri riguardo i pro e i contro di questo approccio, che
ho testato in varie maniere e sembra funzionare egregiamente. Soprattutto mi
chiedo se c'è qualcosa che non è standard e quindi portabile sui vari
compilatori, o dove e in che modo si può migliorare o eliminare ulteriori
"ridondanze".
Supponiamo che io abbia una classe my_int che contenga un valore intero,
accessibile e impostabile attraverso funzioni membro di my_int:
class my_int{
public:
 my_int():_i(0){}
 void set_value(int value){
  _i=value;
 }
 int get_value()const{return _i;}
private:
 int _i;
};
Ora voglio che qualsiasi oggetto che utilizzi un' istanza di questa classe,
e che ne faccia richiesta, venga avvertito di alcuni cambiamenti di stato
relativi a tale istanza di my_int.
Esempio pratico. Voglio che siano segnalati i seguenti "eventi":
1) Quando attraverso set_value si è impostato un valore negativo
2) Quando attraverso set_value si è impostato il valore 5
3) Quando attraverso set_value si è impostato un valore maggiore di 10.
Ognuno di questi tre eventi dovrà generare un "segnale" che sarà catturato
da una apposita funzione membro "annidata" nel codice delle classi
richiedenti, e a ognuna di tali funzioni dovranno essere passati due
parametri: un puntatore all'istanza di my_int che ha generato il segnale e
un intero contenente il valore effettivo di _i dopo la modifica.
Per fare in modo che my_int sia in grado di segnalare i tre cambiamenti di
stato sopra descritti agli oggetti che eventualmente ne vorranno essere
informati, si deve modificare la definizione della classe come segue:
class my_int{
public:
 struct sender_t:dn::slot_t<my_int>{
  virtual void on_negative(parent_t*,int){}
  virtual void on_five(parent_t*,int){}
  virtual void on_moreten(parent_t*,int){}
 } sender;
 my_int():_i(0){sender.link(this);}
 void set_value(int value){
  _i=value;
  if(_i<0)
   sender.raise(&sender_t::on_negative,_i);
  else if(_i==5)
   sender.raise(&sender_t::on_five,_i);
  else if(_i>10)
   sender.raise(&sender_t::on_moreten,_i);
 }
 int get_value()const{return _i;}
private:
 int _i;
};
Alcune cose da notare, in generale:
- Nella sezione pubblica della classe deve essere dichiarata una struttura
annidata che funga da "slot" per l' emissione dei segnali. Tale struttura
deve essere derivata dalla struttura template dn::slot_t con parametro pari
al tipo della nostra classe "emittente" (che è anche la classe padre per lo
slot).
- all' interno di tale struttura andranno inserite le funzioni che saranno
chiamate negli oggetti richiedenti. Le signature delle funzioni sono libere,
tranne per il fatto che il primo parametro dovrà essere sempre un puntatore
alla classe emittente (parent_t è una typedef al tipo della classe padre:
nel caso in esame è equivalente a my_int), e che dovranno essere virtuali
con tipo di ritorno void. Si noti che le funzioni inserite nella struttura
saranno delle definizioni complete, ma esse dovranno necessariamente essere
vuote, cioè consistere esclusivamente di una parentesi graffa aperta seguita
da una parentesi graffa chiusa.
- Sempre nella sezione pubblica, e preferibilmente in modo contestuale alla
definizione della struttura slot, deve essere dichiarato un oggetto del tipo
della struttura slot annidata (nell' esempio è sender).
- In ogni costruttore della classe emittente dovrà essere chiamata la
funzione link dello slot di emissione (sender nell' esempio), passandole il
parametro this: in questo modo lo slot conoscerà l'istanza della classe
emittente in cui esso è contenuto.
- Nei punti in cui si verifica una particolare condizione da segnalare
attraverso una delle funzioni definite nello slot, si emette il segnale
appropriato attraverso la funzione membro raise dello slot stesso, che
riceve come primo argomento un puntatore alla funzione membro dello slot che
si vuole richiamare, e come eventuali parametri successivi, in ordine, gli
argomenti da passare a tale funzione. Tra tali argomenti non va inserito il
puntatore alla classe emittente (this), in quanto ciò è fatto in modo
implicito e trasparente.
Passiamo dunque alla seconda fase, e vediamo in quale modo un oggetto
"ricevente" che voglia essere avvertito dei cambiamenti di stato di my_int
possa eseguire codice ad hoc che venga richiamato al verificarsi dei vari
"eventi" della classe emittente.
Nell' esempio seguente abbiamo una classe chiamata "user" che mantiene al
suo interno un riferimento a un' istanza di my_int passatale nel
costruttore. Gli oggetti di questa classe si limiteranno a segnalare i tre
eventi di my_int sopra descritti visualizzando un messaggio sull' output
standard, insieme al valore assunto dal parametro passato alle funzioni di
segnalazione:
class user{
public:
 user(my_int& mi):_mi(mi){catcher.link(_mi.sender,this);}
private:
 struct catcher_t:dn::slot_t<user,my_int::sender_t>{
  void on_negative(base_t::parent_t* sender,int value){
   std::cout<<"on_negative -> "<<value<<std::endl;
  }
  void on_five(base_t::parent_t* sender,int value){
   std::cout<<"on_five -> "<<value<<std::endl;
  }
  void on_moreten(base_t::parent_t* sender,int value){
   std::cout<<"on_moreten -> "<<value<<std::endl;
  }
 } catcher;
 my_int& _mi;
};
Alcune cose da notare, in generale:
- Deve essere dichiarata una struttura annidata che funga da "slot
ricevente" per i segnali lanciati dalla classe emittente. Tale struttura
deve essere derivata dalla struttura template dn::slot_t col primo parametro
pari al tipo della nostra classe "ricevente" (che è anche la classe padre
per lo slot), ed il secondo pari al tipo dello slot di emissione annidato
nella classe emittente (nel nostro caso "my_int::sender_t"). A questo punto,
nella struttura, parent_t si riferirà al tipo della classe padre (nel nostro
caso user), mentre base_t si riferirà al tipo dello slot emittente
(nell'esempio my_int::sender_t).
- all' interno di tale struttura andranno inserite le funzioni che saranno
chiamate in concomitanza col verificarsi dei vari eventi che la classe
emittente segnalerà. Tali funzioni devono avere lo stesso nome e signature
delle corrispondenti funzioni dello slot emittente (la parola chiave virtual
potrà essere omessa), ma stavolta nel corpo delle funzioni si potrà inserire
il codice che dovrà essere eseguito al verificarsi dei singoli eventi. Non è
necessario inserire tutte le funzioni corrispondenti a quelle dello slot
emittente,ma solo quelle che dovranno intercettare i segnali desiderati. Si
noti inoltre che il tipo del primo parametro delle funzioni è
base_t::parent_t* , che è appunto un puntatore alla classe emittente, e non
più parent_t* , che in questo caso è il tipo della classe ricevente.
- Deve essere dichiarato un oggetto del tipo della struttura slot annidata
(nell' esempio è catcher), preferibilmente in modo contestuale alla
definizione della struttura stessa.
- In ogni costruttore della classe ricevente dovrà essere chiamata la
funzione link dello slot di ricezione (catcher nell' esempio), passandole
come primo parametro lo slot di emissione del quale la nostra classe vuole
ricevere le segnalazioni (nell'esempio _mi.sender), e come secondo parametro
il puntatore all' istanza attuale della classe ricevente (this): in questo
modo lo slot conoscerà sia l'istanza dello slot emittente del quale
intercettare i segnali, sia l'istanza della classe ricevente in cui lo slot
ricevente stesso è contenuto.
Vediamo dunque all'opera il meccanismo nel suo complesso. Nella funzione
main si inserisca il seguente codice:
my_int m_i;
user u(m_i);
m_i.set_value(-2);
m_i.set_value(5);
m_i.set_value(11);
Eseguendo il programma, l'output sarà il seguente:
on_negetive -> -2
on_five -> 5
on_moreten -> 11
Ora, se si commenta la linea "user u(m_i)" , si ricompila e si riesegue il
programma, non verrà visualizzato alcun output, in quanto non esiste più
alcun oggetto che abbia richiesto a m_i di essere avvertito dei suoi
cambiamenti di stato.
Se si aggiungono ulteriori oggetti di tipo user, ognuno visualizzerà i
propri messaggi, in quanto ognuno verrà avvertito del cambiamento di stato.
Analogamente, si sarebbe potuta modificare la classe user in modo che
mantenga riferimenti a più di un oggetto my_int, e linkarli tutti allo slot
ricevente: in tal modo la classe user avrebbe ricevuto i segnali di tutti
gli oggetti my_int, e ancora una volta i messaggi di output si sarebbero
moltiplicati.
Dunque un qualsiasi numero di oggetti riceventi può richiedere di ricevere
segnalazioni da uno stesso oggetto emittente, così come uno stesso oggetto
ricevente può essere collegato a un qualsiasi numero di oggetti emittenti.
Infine ogni struttura derivata nei modi visti da dn::slot_t conterrà un
membro chiamato parent che è un puntatore all'istanza della classe padre che
la contiene (se esso non è stato impostato esplicitamente o attraverso la
funzione membro link, conterrà il valore nullo). Nell'esempio, è possibile
accedere ai membri pubblici di user dall'interno di user::catcher_t
attraverso il puntatore parent (per accedere ai membri privati basta
dichiarare la struttura come friend della classe padre), e analogamente è
possibile accedere ai membri pubblici di my_int dall'interno di
my_int::sender_t attraverso il memro parent di tale struttura (anche se in
questo caso non ha utilità pratica: la struttura my_int::sender_t è
solamente un contenitore di funzioni che non eseguono codice).

 };
diventa:
class my_int{
 public:
  my_int():_i(0), _modified_value_callback(0) {}
  void set_value(int value){
   _i=value;
  if (_modified_value_callback) _modified_value_callback->exec(*this);
  }
  int get_value()const{return _i;}
  void register_modified_value_callback(callback* pCallback)
{_modified_value_callback = pCallback;};
 private:
  int _i;
  callback* _modified_value_callback;
 };
La classe user dovrebbe costruirsi un oggetto class_callback usando se'
stesso, una propria funzione membro da chiamare e l'oggetto emittente ed
infine dovrebbe registrare il puntatore al callback sull'emittente.
A questo punto mi pare che ci siano parecchie similitudini. C'e' pero'
la possibilita' che my_int chiami fuzioni globali se alla funzione di
registrazione si passa un puntatore a global_callback invece di un
puntatore a class_callback.
Ciao
Gian Michele

 Gian Michele
Ciao, Diego

 > segnali "globali".
<snip>
Ok, si possono emettere segnali globali. Pero' le due soluzioni
(passaggio di parametri a parte) come complessita' d'implementazione mi
sembrano sostanzialmente equivalenti. Vediamo i passi da compiere nei
due casi, naturalmente la possibilita' che non abbia capito come
esattamente funzionano le cose e' altissima :-)
tua soluzione, oggetto emittente
a) creare una struttura sender_t:dn::slot_t pubblica all'interno della
classe
b) riempirla con le funzioni che fungono da segnali
c) richiamare le funzioni segnali quando opportuno
mia soluzione, oggetto emittente
a) creare un puntatore privato a un oggetto callback e inizializzarlo
nel costruttore per ogni segnale
b) fornire una funzione di registrazione per i client
c) chiamare il metodo exec se il puntatore non e' null
Fin qui le cose mi sembrano equivalenti, a parte una maggior
incapsulazione nel mio caso. Tieni inoltre conto che callback* puo'
essere incapsulato a sua volta in una classe p.e. emitter che fornisce
inizializzazione automatica, registrazione e chiamata di exec
controllata. La funzione di registrazione viene resa pubblica
indirettamente delegando la chiamata all'oggetto emitter.
I tre punti rimangono:
a) inserire uno o piu' oggetti emitter
b) fornire funzioni di registrazione
c) generare i segnali
Qua sarebbe interessante definire un meccanismo d'interrogazione da
parte del ricevente per enumerare i segnali supportati. E' chiaro che i
miei oggetti emitter possono essere anche creati dinamicamente e forniti
da un oggetto factory.
Vediamo adesso il ricevente
tua soluzione, oggetto ricevente
a) creare una struttura privata catcher_t:dn::slot_t
b) riempirla con le funzioni evento
c) chiamare link
mia soluzione, oggetto ricevente
a) creare le funzioni evento
b) creare uno o piu' oggetti class_callback
c) registrare gli oggetti class_callback
Anche qua mi sembra ci siano diverse analogie. Una cosa che forse non si
puo' fare nel tuo caso e' connettere due o piu' segnali dell'emittente
ad un unico slot, ma se non e' cosi' perdona la mia poca comprensione
:-)
E veniamo ai parametri.
Nel mio caso non puoi definire segnali dotati di parametri, ma l'unico
parametro passato e' un reference all'oggetto emittente (in pratica
l'emittente "lancia" se' stesso insieme al segnale). Questa e' una
precisa scelta ("non e' un difetto, ma una feature" ^__^) per non
vincolare troppo la struttura della classe emittente con quella della
classe ricevente. Infatti questa caratteristica e' fornita dall'oggetto
template class_callback e non dall'emittente.
Per adesso non mi viene in mente altro.
Ciao
Gian Michele
P.S. Il codice immagino che possa interessare a molti e, in ogni caso,
sono sempre occasioni per imparare.

 sono sempre occasioni per imparare.
Incollo il codice (con commenti non dettagliati, ma non dovrebbe essere
difficile capire il meccanismo) in un nuovo messaggio... sperando
soprattutto di ottenere delucidazioni sulla sua correttezza e conformità
allo standard (io uso il vc6, che notoriamente non è molto aderente allo
standard).

 disaccoppiamento è totale.
Probabilmente non ho capito come funziona :-) Spero tuttavia di averti
fornito qualche spunto almeno tanto quanto tu ne hai fornito a me.
Ciao
Gian Michele