giovedì 30 aprile 2009

Qt: la gerarchia degli oggetti

La libreria Qt prevede che ogni oggetto abbia un parent, ossia un genitore responsabile della sua vita. Questa scelta è molto importante se si considera che Qt lavora in C++, dove la gestione della memoria è affidata al programmatore. Per ovviare alla proliferazione di oggetti "dimenticati" (e quindi memory leak), Qt adotta la scelta di organizzare gli oggetti a run-time in un albero, in modo che ogni oggetto sia sempre raggiungibile tramite un nodo padre (il parent appunto). Avendo un albero di oggetti è possibile quindi eliminare un intero sottoalbero agendo sul nodo padre, ed è proprio quello che avviene in Qt: ogni volta che viene eliminato un parent tutti i suoi figli sono eliminati. Questo significa che l'operazione di delete su un oggetto Qt implicherà una delete ricorsiva su tutti gli oggetti che hanno il primo come parent.
Per gli sviluppatori della TrollTech, questa scelta è così importante che ogni oggetto Qt prevede, nel costruttore, un riferimento ad un oggetto padre, che diventerà il responsabile della distruzione dell'oggetto stesso.
A prima vista questo puo' sembrare un po' vincolante nel codice, infatti supponendo di avere un pannello al quale si vogliono aggiungere tre pulsanti, il codice appare come:

QWidget* container = new QWidget();
QPushButton* b1 = new QPushButton("Pulsante 1", container);
QPushButton* b2 = new QPushButton("Pulsante 2", container);
QPushButton* b3 = new QPushButton("Pulsante 3", container);

Come si nota, ogni oggetto contenuto (QPushButton) deve specificare il proprio parent (in questo caso container). Si noti che è possibile costruire un oggetto senza specificare il nodo padre, ed è poi possibile specificarlo in un secondo momento con il metodo setParent(), ad ogni modo la scelta di includere il parent nel costruttore è una sottolineatura di come sia importante, nella gestione della memoria Qt, la costruzione dell'albero degli oggetti.

Confrontando questo approccio con quello Java, si potrebbe storcere il naso, poiché quest'ultimo risulta piu' intuitivo da leggere. Il codice corrispondente all'aggiunta dei tre pulsanti ad un pannello risulta infatti:

JPanel container = new JPanel();
JButton b1 = new JButton("Pulsante 1");
JButton b2 = new JButton("Pulsante 2");
JButton b3 = new JButton("Pulsante 3");
container.add( b1 );
container.add( b2 );
container.add( b3 );

o anche in modo equivalente:

JButton b1 = new JButton("Pulsante 1");
JButton b2 = new JButton("Pulsante 2");
JButton b3 = new JButton("Pulsante 3");
JPanel container = new JPanel();
container.add( b1 );
container.add( b2 );
container.add( b3 );

La differenza rispetto all'approccio Qt è che in Java è possibile costruire i singoli componenti e aggiungerli semplicemente al contenitore in un secondo momento, mentre in Qt è necessario definire prima il contenitore e poi specificare, per ogni componente, dove questo deve essere contenuto. Se è vero che anche in Qt si sarebbe potuto scrivere:



QPushButton* b1 = new QPushButton("Pulsante 1");
QPushButton* b2 = new QPushButton("Pulsante 2");
QPushButton* b3 = new QPushButton("Pulsante 3");
QWidget* container = new QWidget();
b1->setParent( container );
b2->setParent( container );
b3->setParent( container );

è pur vero che questa tecnica è scarsamente usata e incoraggiata. La ragione risiede proprio in cio' che è stato detto prima: Qt forza la definizione della relazione fra gli oggetti fin dalla loro costruzione, al fine di impostare correttamente da subito la gestione futura della memoria.
Per meglio comprendere l'importanza di cio', si consideri il seguente esempio Java e il corrispettivo Qt (in stile Java):

JButton b1 = new JButton("Pulsante 1");
JButton b2 = new JButton("Pulsante 2");
JButton b3 = new JButton("Pulsante 3");
JPanel container = new JPanel();
container.add( b1 );
container.add( b2 );

e in Qt stile Java:

QPushButton* b1 = new QPushButton("Pulsante 1");
QPushButton* b2 = new QPushButton("Pulsante 2");
QPushButton* b3 = new QPushButton("Pulsante 3");
QWidget* container = new QWidget();
b1->setParent( container );
b2->setParent( container );

rispetto agli esempi di prima, qui vengono creati tre pulsanti, ma solo due sono effettivamente aggiunti al container, mentre il terzo viene "dimenticato". Ma se in Java scatta il garbage collector a recuperare la memoria del terzo pulsante, in C++ nulla del genere avviene, e siccome il terzo pulsante non ha nessun padre, non viene eliminato automaticamente mai, nemmeno quando il container cessa la sua vita. Ne consegue che così facendo si rischia di avere dei memory leak. Ecco quindi che Qt ovvia al problema imponendo, a tempo di costruzione degli oggetti, di specificare esattamente le relazioni che intercorrono fra di essi. Solo così l'albero degli oggetti puo' essere creato e mantenuto.

Si noti poi che ogni Widget prevede, come valore di default, un puntatore nullo ad un parent widget. Come conseguenza, un widget puo' sempre essere costruito senza parent, anche se questo richiede maggiore attanzione al programmatore per la gestione della memoria.

Da ultimo, Qt mette a disposizione una classe particolare QPointer, che agisce come un puntatore tipizzato ad un QObject. La differenza fra un QPointer e un puntatore normale è che il primo reagisce (tramite uno slot) quando il componente al quale punta viene cancellato. Infatti il QPointer viene azzerato (impostato a null), in modo da indicare che non punta piu' a nessun oggetto. Lo scopo è quello di consentire ad un programmatore di mantenere puntatori sicuri a oggetti nella gerarchia degli oggetti Qt, in modo che se un oggetto viene cancellato (poiché lo è il suo parent), il puntatore venga immediatamente invalidato.

Nessun commento: