User:Fresbeeplayer/Development/Tutorials/Common Programming Mistakes (it)
Collezione di Tutorial | Getting Started |
Prerequisiti | Nessuno |
A Seguire | n/a |
Ulteriori Letture | APIs to avoid |
Prefazione
Questo tutorial mira a combinare le esperienze degli sviluppatori KDE su cosa fare e cosa non fare in merito alle librerie Qt e KDE. Oltre agli errori, vengono coperte anche cose che non sono necessariamente "bachi" ma che rendono il codice più lento e di difficile lettura.
C++ in generale
Questa sezione ti guida attraverso alcuni degli angoli più remoti del C++ che tendono ad essere mal utilizzati o dei quali la gente si sbaglia.
Namespace anonimi contro static
<! -- controllare il passaggio file-static If you have a method in a class that does not access any members and therefore does not need an object to operate, make it static. If additionally it is a private helper function that is not needed outside of the file, make it a file-static function. That hides the symbol completely. -->
Se hai un metodo in una classe che non accede ad alcun membro e quindi non ha bisogno di un oggetto per funzionare, rendilo statico. Se in più è una funzione di supporto privata che non viene utilizzata all'esterno del file, rendila file-static. In questo modo essa viene nascosta completamente.
<! -- un namespace anonimo non ha linkage interno? Symbols defined in a C++ anonymous namespace do not have internal linkage. Anonymous namespaces only give a unique name for that translation unit and that is it; they do not change the linkage of the symbol at all. Linkage is not changed on those because the second phase of two-phase name lookup ignores functions with internal linkages. Also, entities with internal linkage cannot be used as template arguments. -->
Le entità definite in un namespace anonimo in C++ non hanno linkage interni. I namespace anonimi offrono soltanto un nome unico per quella translation unit e basta; non cambiano in nessun modo il linkage dell'identificatore. Il linkage non viene cambiato perché la seconda delle due fasi di ricerca dei nomi ignora le funzioni con linkage interno. Per di più, le entità con linkage interno non possono essere usate come argomento di un template.
A questo punto invece di usare namespace anonimi usa la parola chiave static se non vuoi che un simbolo venga esportato.
Problemi di puntatore Nullo
Prima di tutto: va bene eliminare un puntatore nullo. Quindi costruttori come il seguente che controllano che il valore sia nullo prima di eliminarlo sono semplicemente ridondanti:
if ( ptr ) {
delete ptr;
}
Da notare comunque, che un controllo per valore nullo è richiesto quando cancelli un array - questo perché altrimenti un compilatore relativamente recente per Solaris non lo gestisce opportunamente.
Quando elimini un puntatore, assicurati anche di settarlo a 0 in modo che futuri tentativi di cancellazione non falliscano in una doppia eliminazione. Per cui il modo completo e corretto di procedere è:
delete ptr;
ptr = 0;
Potresti notare che i puntatori nulli sono variamente indicati in uno di questi tre modi: 0, 0L e NULL. In C, NULL è definito come un puntatore nullo di tipo void. Ma in C++ ciò non è possibile a causa di un controllo di tipo più stretto. Perciò, moderne implementazioni del C++ lo rendono come un "magico" puntatore nullo costante il quale può essere assegnato a qualunque altro puntatore. D'altra parte le implementazioni più vecchie di C++ semplicemente lo associano a 0 o 0L, il quale non tiene conto di alcuna sicurezza di tipo - si potrebbe assegnarlo ad una variabile intera, ovviamente sbagliando.
Nel contesto dei puntatori, la costante intera zero significa "puntatore nullo" - irrispettoso della rappresentazione binaria di un puntatore nullo. Ciò significa che la scelta tra 0, 0L e NULL è una questione di stile personale e abitudine piuttosto che tecnica - fintantoché nel codice SVN di KDE vedrai 0 usato più comunemente di NULL.
Notare, comunque, che se vuoi passare un puntatore nullo costante ad una funzione nella lista delle variabili degli argomenti, *devi* esplicitamente farne il cast in un puntatore - il compilatore assume di default il contesto degli interi, il quale può o non può coincidere con la rappresentazione binaria di un puntatore. Di nuovo, non ha importanza il fatto che fai il cast a 0, 0L o NULL, ma la rappresentazione più corta è generalmente preferita.
Variabili membro
Incontrerai quattro maggiori stili per segnare le variabili membre delle classi in KDE:
- m_variabile m minuscola, underscore ed il nome della variabile che comincia con una lettera minuscola. Questo è lo stile più comune ed uno dei preferiti nel codice delle kdelibs.
- mVariabile m minuscola ed il nome della variabile che comincia con una lettera maiuscola.
- variabile_ il nome della variabile comincia con la lettera minuscola ed alla fine un underscore.
- _variabile un underscore e poi il nome della variabile con la lettera iniziale minuscola. Questa notazione di solito è sconsigliata siccome è anche utilizzata in qualche codice per i parametri delle funzioni.
Come accade spesso non c'è un modo corretto per farlo, perciò ricorda sempre di rispettare la sintassi utilizzata dall'applicazione/libreria alla quale stai facendo commit.
Variabili statiche
Cerca di limitare il numero di variabili statiche nel tuo codice, specialmente quando fate il commit per una libreria. Costruzione ed inizializzazione di un grande numero di variabili statiche fa veramente male ai tempi di avvio.
<! -- ricontrollare Do not use class-static variables, especially not in libraries and loadable modules though it is even discouraged in applications. Static objects lead to lots of problems such as hard to debug crashes due to undefined order of construction/destruction. -->
Non usare variabili class-static, in particolare non nelle librerie e nei moduli sebbene sia anche scoraggiato nelle applicazioni. Oggetti statici portano ad un sacco di problemi tra cui difficoltà di debug dei crash dovuto ad un ordine indefinito di costruttore/distruttore.
Invece, usa un puntatore statico insieme a K_GLOBAL_STATIC definito in kglobal.h ed usato in questo modo:
class A { ... };
K_GLOBAL_STATIC(A, globaleA)
void faQualcosa()
{
A *a = globaleA;
...
}
void faQualcosAltro()
{
if (globaleA.isDestroyed()) {
return;
}
A *a = globaleA;
...
}
void installaPostRoutine()
{
qAddPostRoutine(globaleA.destroy);
}
Vedi la documentazione delle API per più informazioni su K_GLOBAL_STATIC.
Dati costanti
Se hai bisogno di qualche dato costante per semplici tipi di dato in molti punti, fai bene a definirli una volta sola in un posto centrale, onde evitare errori di digitazione in una delle istanze. Se i dati cambiano hai bisogno di editare solo in un punto.
Anche se usati una sola volta è meglio definirli da un'altra parte, per evitare inspiegabili "numeri magici" nel codice (cmp. 42). Di solito ciò viene fatto in cima al file per non doverli ricercare.
Definisci i dati costanti usando i costrutti del C++, non le istruzioni del preprocessore, come potresti essere abituato a fare dal C. In questo modo il compilatore può aiutarti a trovare errori facendo il controllo di tipo.
// Corretto!
static const int LaRispostaATutteLeDomande = 42;
// Sbagliato!
- define LaRispostaATutteLeDomande 42
<! -- ricontrollare If defining a constant array do not use a pointer as data type. Instead use the data type and append the array symbol with undefined length, [], behind the name. Otherwise you also define a variable to some const data. That variable could mistakenly be assigned a new pointer to, without the compiler complaining about. And accessing the array would have one indirection, because first the value of the variable needs to be read. -->
Se stai definendo un array costante non usare un puntatore come tipo di dato. Invece usa il suo tipo ed appendi il simbolo dell'array di indefinita lunghezza, [], dopo il nome. Altrimenti definirai anche una variabile con qualche dato costante. La variabile potrebbe per sbaglio essere assegnata ad un altro puntatore, senza che il compilatore se ne lamenti. E l'accesso all'array sarebbe indiretto, perché per primo deve essere letto il valore della variabile.
// Corretto!
static const char UnaStringa[] = "Esempio";
// Sbagliato!
static const char* UnaStringa = "Esempio";
// Sbagliato!
- define UnaStringa "Esempio"
Dichiarazioni anticipate
Ridurrai i tempi di compilazione dichiarando anticipatamente le classi quando possibile invece di includere i rispettivi headers. Per esempio:
- include <QWidget> // lento
- include <QStringList> // lento
- include <QString> // lento
class QualcheInterfaccia
{
public:
virtual void azioneWidget( QWidget *widget ) =0;
virtual void azioneStringa( const QString& str ) =0;
virtual void azioniListaStringhe( const QStringList& strLista ) =0;
};
Dovrebbe invece essere scritto in questo modo:
class QWidget; // veloce
class QStringList; // veloce
class QString; // veloce
class QualcheInterfaccia
{
public:
virtual void azioneWidget( QWidget *widget ) =0;
virtual void azioneStringa( const QString& str ) =0;
virtual void azioniListaStringhe( const QStringList& strLista ) =0;
};
Iteratori
Preferire iteratori costanti e conservare end()
Preferisci l'uso dei const_iterators rispetto ai normali iteratori quando possibile. I containers, implicitamente condivisi, spesso eseguono un detach() (vedi [1] per più informazioni, n.d.t.) quando viene fatta una chiamata ad un metodo begin() o end() non costanti (QList è un esempio di tale container). Usando i const_iterator assicurati di stare chiamando la versione costante di begin() e end(); altrimenti, a meno che il tuo container sia esso stesso costante, potrebbero esserci detach non necessari del tuo container. Fondamentalmente ogni qual volta usi const_iterator inizializzalo usando constBegin()/constEnd(), per stare sul sicuro.
Conserva il valore di ritorno del metodo end() (o constEnd()) prima di iterare su un grande container. Per esempio:
QList<QualcheClasse> container;
// codice che inserisce un grande numero di elementi nel container
QList<QualcheClasse>::ConstIterator end = container.constEnd();
QList<QualcheClasse>::ConstIterator itr = container.constBegin();
for ( ; itr != end; ++itr ) {
// usa *itr (oppure itr.value()) qui dentro
}
Questo evita la creazione non necessaria di un oggetto temporaneo ritornato da end() (o constEnd()) ad ogni iterazione del ciclo, velocizzandolo ampiamente.
Ogni volta che usi gli iteratori, utilizza sempre operatori di pre-incremento e pre-decremento (ad esempio, ++itr) a meno di avere uno motivo specifico per non farlo. L'utilizzo di operatori di post-incrementi e post-decrementi (come itr++) causano la creazione di un oggetto temporaneo.
Fai attenzione quando cancelli elementi dentro un ciclo
Quando vuoi cancellare qualche elemento dalla lista, vorresti usare codice simile a questo:
<! -- approfondire timer e job -->
QMap<int, Job *>::iterator it = m_activeTimers.begin();
QMap<int, Job *>::iterator itEnd = m_activeTimers.end();
for( ; it!=itEnd ; ++it )
{
if(it.value() == job)
{
// Trovato un timer per questo job. Fermiamolo.
killTimer(it.key());
m_activeTimers.erase(it);
}
}
Questo codice può potenzialmente andare in crash a causa dell'iteratore pendente dopo la chiamata a erase(). Devi riscrivere il codice in questo modo:
QMap<int, Job *>::iterator it = m_activeTimers.begin();
while (it != m_activeTimers.end())
{
QMap<int, Job *>::iterator prev = it;
++it;
if(prev.value() == job)
{
// Trovato un timer per questo job. Fermiamolo.
killTimer(prev.key());
m_activeTimers.erase(prev);
}
}
Questo problema è anche discusso nella documentazione Qt di QMap::iterator ma si applica a tutti gli iteratori delle Qt.
Falle nella memoria
Un errore di programmazione molto "popolare" consiste nel fare un new senza un delete come in questo programma:
mem_buongustaio.cpp
class t
{
public:
t() {}
};
void inquina()
{
t* inquinatore = new t();
}
int main()
{
while (true) inquina();
}
Come puoi vedere, inquina() istanzia un nuovo oggetto inquinatore di tipo t. Quindi, la variabile inquinatore viene persa dato che è locale, ma il contenuto (l'oggetto) rimane nello heap. Posso usare questo programma per rendere il mio computer inutilizzabile in 10 secondi.
Per risolvere, ci sono i seguenti approcci:
- tieni la variabile nello stack invece che nello heap:
t* inquinatore = new t();
diventerà
t inquinatore();
- cancella l'inquinatore usando la funzione complementare a new:
delete inquinatore;
Uno strumento per individuare le falle di memoria come queste è Valgrind.
dynamic_cast
Puoi fare un dynamic_cast al tipo T dal tipo T2 tali che:
<! -- ricontrollare e approfondire
- T is defined in a library you link to (you'd get a linker error if this isn't the case, since it won't find
the vtable or RTTI info)
- T is "well-anchored" in that library. By "well-anchored" I mean that the vtable is not a COMMON symbol subject to merging at run-time by the dynamic linker. In other words, the first virtual member in the class definition must exist and not be inlined: it must be in a .cpp file.
- T and T2 are exported -->
- T è definito in una libreria a cui fai il link (avrai un errore dal linker se non è così, dal momento che non troverà le informazioni vtable e RTTI)
- T è "ben-ancorato" in quella libreria. Con "ben-ancorato" intendo che vtable non è un simbolo COMUNE soggetto a fusioni a run-time da parte del linker dinamico. In altre parole, il primo membro virtuale nella definizione della classe deve esistere e non essere inline: deve essere in un file .cpp.
- T e T2 sono esportati.
Per esempio, noi abbiamo incontrato qualche problema difficile da individuare nel codice C++ non KDE (NMM credo) a cui stavamo facendo il link:
- libphonon carica il plugin NMM
- il plugin NMM fa il link a NMM
- NMM carica i suoi plugins
- i plugins propri di NMM, fanno il link a NMM
Qualche classe nella libreria di NMM non aveva vtables ben-ancorate, così il dynamic_casting falliva dentro al plugin NMM di Phonon per gli oggetti creatii nei plugins di NMM
Progettazione delle applicazioni
In questa sezione copriremo un po' di problemi comuni relativi alla progettazione di applicazioni Qt/KDE.
Inizializzazione ritardata
Sebbene il design di moderne applicazioni C++ può essere molto complesso, un problema ricorrente, generalmente facile da sistemare, è il non usare la tecnica dell'inizializzazione ritardata.
Per prima cosa, diamo un'occhiata al modo standard di inizializzare un'applicazione KDE:
int main( int argc, char **argv )
{
....
KApplication a;
KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
MainWindow *window = new MainWindow( args );
a.setMainWidget( window );
window->show();
return a.exec();
}
Nota che window viene creata prima di a.exec() il quale fa partire il ciclo degli eventi. Ciò implica che vogliamo evitare di fare cose non banali nella parte alta del costruttore, visto che verranno eseguite prima ancora che la finestra venga mostrata.
La soluzione è semplice: abbiamo bisogno di ritardare la costruzione di qualunque cosa oltre alla GUI fino a che il ciclo degli eventi sia partito. Qui di seguito è mostrato come il costruttore della classe MainWindow dovrebbe essere per ottenere questo risultato:
MainWindow::MainWindow()
{
initGUI();
QTimer::singleShot( 0, this, SLOT(initObject()) );
}
void MainWindow::initGUI()
{
/* Costruisci i tuoi widget qui. Nota che non devono
* richiedere una complessa inizializzazione,
* o verrà meno lo scopo di questa tecnica.
* Tutto ciò che vorresti fare è creare i tuoi oggetti
* della GUI ed usare QObject::connect per connettere
* i segnali agli slots appropriati.
*/
}
void MainWindow::initObject()
{
/* Questo slot sarà chiamato non appena parte il ciclo eventi.
* Metti tutto il resto che deve essere fatto, compresi
* assegnazione di valori, lettura files, ristebilire sessioni, etc...
* Tutto ciò prenderà lo stesso del tempo, ma almeno la tua
* finestra sarà visibile a schermo, facendo apparire
* attiva l'applicazione.
*/
}
Usare questa tecnica potrebbe non far risparmiare del tempo in più, ma farà sembrare più veloce l'applicazione agli utenti che la eseguono. Questa percezione di reattività incrementata è rassicurante per l'utente il quale riceve un rapido feedback per il riuscito avvio dell'applicazione.
Quando (e solo in questo caso) l'avvio non può essere reso ragionevolmente abbastanza veloce, considera l'uso di KSplashScreen.
Strutture Dati
In questa sezione spazieremo su alcune delle nostre più comuni persecuzioni che affliggono le più comuni strutture dati viste nelle applicazioni Qt/KDE.
Passaggio di tipi non POD
I tipi di dato diversi da POD ("Plain Old Data", dati semplici senza la logica di controllo) dovrebbero essere passati sempre per riferimento costante. Questo include qualunque cosa tranne i tipi base come char e int.
Prendi, per esempio, QString. Dovrebbero sempre essere passati ai metodi come const QString&. Anche se QString è implicitamente condiviso è comunque più efficiente (e sicuro) passarlo per referenza costante piuttosto che come oggetto per valore.
Quindi la dichiarazione canonica di un metodo che prende QString come argomento è:
void mioMetodo( const QString & x, const QString & y );
QObject
Se avrai mai bisogno di eliminare una classe derivata da QObject dall'interno di uno dei suoi metodi, non farlo mai in questo modo:
delete this;
Prima o poi ciò causerà un crash perché un metodo di quell'oggetto potrebbe essere invocato dal ciclo eventi delle Qt via slots/signals dopo che tu l'hai eliminato.
Invece usa sempre QObject::deleteLater() il quale cerca di fare la stessa cosa di delete this ma in modo più sicuro.
QStrings vuote
Si è soliti voler verificare se un QString è vuoto. Di seguito ci sono tre modi per farlo, dei quali i primi due sono corretti:
// Corretto
if ( miaStringa.isEmpty() ) {
}
// Corretto
if ( miaStringa == QString() ) {
}
// Sbagliato! ""
if ( miaStringa == "" ) {
}
Mentre c'è distinzione tra QString nulle e vuote, ciò è puramente un artefatto storico e per il nuovo codice se ne scoraggia l'uso.
QString e lettura da file
Se stai leggendo un file, è più veloce convertirlo dalla codifica locale a Unicode (QString) tutto in una volta, piuttosto che riga per riga. Questo vuol dire che metodi come QIODevice::readAll() sono spesso una buona soluzione, seguiti da una singola istanza di QString.
Per file molto grandi, considera la possibilità di leggere un blocco di righe e quindi eseguire la conversione. In questo modo hai l'opportunità di aggiornare la GUI. Ciò può essere fatto rientrando normalmente nel ciclo eventi, contemporaneamente utilizzando un timer per leggere i blocchi in background, oppure creando un ciclo eventi locale.
Anche se si potrebbe usare anche qApp->processEvents(), è scoraggiato siccome porta facilmente a subdoli problemi fatali.
Leggere QString da un KProcess
KProcess emette il segnale readyReadStandard{Output|Error} appena arrivano dei dati. Un errore comune sta nel leggere tutti i dati disponibili nello slot connesso e convertirli in un QString così come sono: i dati arrivano arbitrariamente segmentati, così caratteri multi-byte potrebbero essere tagliati in pezzi e perciò invalidati. Esistono molti approcci al problema:
- Hai veramente bisogno di processare i dati che arrivano? Se no, basta utilizzare readAllStandard{Output|Error} dopo che il processo è uscito. Diversamente da KDE3, KProcess è ora capace di accumulare i dati per te.
- Racchiudi il processo in un QTextStream e leggi intere righe. Questo dovrebbe funzionare a partire dalle Qt 4.4
- Accumula i pezzi di dati negli slots e processali ogni volta che arriva una riga e dopo un certo timeout. Codice di esempio
QString e QByteArray
Mentre QString è lo strumento scelto per molte situazioni che richiedono la gestione di stringhe, ce n'è una dove è particolarmente inefficiente. Se stai lavorando con dati in un QByteArray, fai attenzione a non passarlo a metodi che prendono parametri di tipo QString, e che ne tirano fuori un QByteArrays di nuovo.
Per esempio:
QByteArray mieiDati;
QString mieiNuoviDati = maciullaDati( mieiDati );
QString maciullaDati( const QString& dati ) {
QByteArray str = dati.toLatin1();
// maciulla
return QString(str);
}
L'operazione dispendiosa che si verifica è la conversione in QString, la quale internamente fa conversioni a Unicode. Ciò è inutile perché la prima cosa che il metodo fa è riconvertirla di nuovo con toLatin1(). Quindi, se sei sicuro che la conversione a Unicode non è necessaria, cerca di evitare di usare inavvertitamente QString.
L'esempio di cui sopra dovrebbe invece essere scritto come:
QByteArray mieiDati;
QByteArray mieiNuoviDati = maciullaDati( mieiDati );
QByteArray maciullaDati( const QByteArray& dati )
QDomElement
Quando si deve fare il paring di documenti XML, si ha bisogno di iterare su tutti gli elementi. Potresti essere tentato di usare il seguente codice:
for ( QDomElement e = baseElement.firstChild().toElement();
!e.isNull();
e = e.nextSibling().toElement() ) {
...
}
Ebbene non è corretto: questo ciclo si fermerà prematuramente non appena incontra un QDomNode che è qualcosa di diverso da un elemento, come un commento.
Il ciclo corretto è qualcosa di simile:
for ( QDomNode n = baseElement.firstChild(); !n.isNull();
n = n.nextSibling() ) {
QDomElement e = n.toElement();
if ( e.isNull() ) {
continue;
}
...
}