Ottimizzare C++/Scrivere codice C++ efficiente/Costruzioni e distruzioni

Indice del libro

La costruzione e la distruzione di un oggetto richiedono tempo, particolarmente se tale oggetto, direttamente o indirettamente, possiede altri oggetti.

In questa sezione vengono proposte linee-guida per ridurre il numero di creazioni di oggetti, e quindi delle loro corrispondenti distruzioni.

Ambito delle variabili

modifica

Dichiara le variabili il più tardi possibile.

Dichiarare una variabile il più tardi possibile significa sia dichiararla nell'ambito (in inglese, scope) più stretto possibile, sia dichiararla il più avanti possibile entro quell'ambito. Essere nell'ambito più stretto possibile comporta che se tale ambito non viene mai raggiunto, l'oggetto associato alla variabile non viene mai costruito né distrutto. Dichiarare una variabile il più avanti possibile all'interno di un ambito comporta che se prima di tale dichiarazione c'è un'uscita prematura, tramite return o break o continue, l'oggetto associato alla variabile non viene mai né costruito né distrutto.

Inoltre spesso all'inizio di una routine non si ha un valore appropriato per inizializzare l'oggetto associato alla variabile e quindi si è costretti a inizializzarla con un valore di default per poi assegnarle il valore appropriato. Se invece la si dichiara quando si ha a disposizione il valore appropriato, la si può inizializzare con tale valore senza bisogno di fare un successivo assegnamento, come suggerito dalla linea-guida "Inizializzazioni" in questa sezione.

Inizializzazioni

modifica

Usa inizializzazioni invece di assegnamenti. In particolare, nei costruttori usa le liste di inizializzazione.

Per esempio, invece di scrivere:

string s;
...
s = "abc"

scrivi:

string s("abc");

Anche se un'istanza di una classe non viene inizializzata esplicitamente, viene comunque inizializzata automaticamente dal costruttore di default.

Chiamare il costruttore di default seguito da un assegnamento di un valore può essere meno efficiente che chiamare solo un costruttore con tale valore.

Operatori di incremento/decremento

modifica

Usa gli operatori prefissi di incremento (++) o decremento (--) invece dei corrispondenti operatori postfissi, se il valore dell'espressione non viene usato.

Se l'oggetto incrementato è di un tipo fondamentale, non ci sono differenze tra le due forme, ma se si tratta di un tipo composto, l'operatore postfisso comporta la creazione di un oggetto temporaneo, mentre l'operatore prefisso no.

Siccome ogni oggetto che è attualmente di un tipo fondamentale potrebbe diventare in futuro di una classe, è bene usare sempre l'operatore che è comunque più efficiente.

Tuttavia, se il valore dell'espressione formata dall'operatore di incremento o decremento viene usata in un'espressione più grande, potrebbe essere opportuno usare l'operatore postfisso.

Operatori compositi di assegnamento

modifica

Usa gli operatori compositi di assegnamento (come in a += b) invece degli operatori semplici combinati con operatori di assegnamento (come in a = a + b).

Per esempio, invece del seguente codice:

string s1("abc");
string s2 = s1 + " " + s1;

scrivi il seguente codice:

string s1("abc");
string s2 = s1;
s2 += " ";
s2 += s1;

Tipicamente un operatore semplice, crea un oggetto temporaneo. Nell'esempio, gli operatori + creano stringhe temporanee, la cui creazione e distruzione richiede tempo.

Al contrario, il codice equivalente che usa l'operatore += non crea oggetti temporanei.

Passaggio di argomenti alle funzioni

modifica

Quando devi passare un argomento x di tipo T a una funzione, usa il seguente criterio:

  • Se x è un argomento di solo input,
    • se x può essere nullo,
      • passalo per puntatore a costante (const T* x),
    • altrimenti, se T è un tipo fondamentale o un iteratore o un oggetto-funzione,
      • passalo per valore (T x) o per valore costante (const T x),
    • altrimenti,
      • passalo per riferimento a costante (const T& x),
  • altrimenti, cioè se x è un argomento di solo output o di input/output,
    • se x può essere nullo,
      • passalo per puntatore a non-costante (T* x),
    • altrimenti,
      • passalo per riferimento a non-costante (T& x).

Il passaggio per riferimento è più efficiente del passaggio per puntatore in quanto facilita al compilatore l'eliminazione della variabile, e in quanto il chiamato non deve verificare se il riferimento è valido o nullo; tuttavia, il puntatore ha il pregio di poter rappresentare un valore nullo, ed è più efficiente passare solo un puntatore, che un riferimento a un oggetto insieme a un booleano che indica se tale riferimento è valido.

Per oggetti che possono essere contenuti in uno o due registri, il passaggio per valore è più efficiente o ugualmente efficiente del passaggio per riferimento, in quanto tali oggetti possono essere contenuti in registri e non hanno livelli di indirettezza, pertanto questo è il modo più efficiente di passare oggetti sicuramente piccoli, come i tipi fondamentali, gli iteratori e gli oggetti-funzione. Per oggetti più grandi di due registri, il passaggio per riferimento è più efficiente del passaggio per valore, in quanto il passaggio per valore comporta la copia di tali oggetti nello stack.

Un oggetto composito veloce da copiare potrebbe essere efficientemente passato per valore, ma, a meno che si tratti di un iteratore o di un oggetto-funzione, per i quali si assume l’efficienza della copia, tale tecnica è rischiosa, in quanto l’oggetto potrebbe diventare in futuro più lento da copiare. Per esempio, se un oggetto di classe Point contiene solo due float, potrebbe essere efficientemente passato per valore; ma se in futuro si aggiungesse un terzo float, o se i due float diventassero due double, potrebbe diventare più efficiente il passaggio per riferimento.

Dichiarazione explicit

modifica

Dichiara explicit tutti i costruttori che possono ricevere un solo argomento, eccetto i costruttori di copia delle classi concrete.

I costruttori non-explicit possono essere chiamati automaticamente dal compilatore che esegue una conversione automatica. L'esecuzione di tale costruttore può richiedere molto tempo.

Se tale conversione è resa obbligatoriamente esplicita, e il nome della classe destinazione non viene specificato nel codice, il compilatore potrebbe scegliere un'altra funzione in overload, evitando così di chiamare il costoso costruttore, oppure segnalare l'errore e costringere il programmatore a scegliere un'altra strada per evitare la chiamata al costruttore.

Per i costruttori di copia delle classi concrete si deve fare eccezione, per consentirne il passaggio per valore. Per le classi astratte, anche i costruttori di copia possono essere dichiarati explicit, in quanto, per definizione, le classi astratte non si possono istanziare, e quindi gli oggetti di tale tipo non dovrebbero mai essere passati per valore.

Operatori di conversione

modifica

Dichiara operatori di conversione solamente per mantenere la compatibilità con una libreria obsoleta (in C++0x, dichiarali explicit).

Gli operatori di conversione consentono conversioni implicite, e quindi incorrono nello stesso problema dei costruttori impliciti, descritto nella linea-guida "Dichiarazione explicit" di questa sezione.

Se tali conversioni sono necessarie, fornisci invece una funzione membro equivalente, che può essere chiamata solo esplicitamente.

L'unico utilizzo che rimane accettabile per gli operatori di conversione si ha quando si vuole far convivere una nuova libreria con un'altra vecchia libreria simile. In tal caso, può essere comodo avere operatori che convertono automaticamente gli oggetti dai tipi della vecchia libreria ai tipi della nuova libreria e viceversa.

Idioma Pimpl

modifica

Usa l'idioma Pimpl solamente quando vuoi rendere il resto del programma indipendente dall'implementazione di una classe.

L'idioma Pimpl (che significa Puntatore a IMPLementazione) consiste nel memorizzare nell'oggetto solamente un puntatore alla struttura che contiene tutte le informazioni utili di tale oggetto.

Il vantaggio principale di tale idioma è che velocizza la compilazione incrementale del codice, cioè rende meno probabile che una piccola modifica ai sorgenti comporti la necessità di ricompilare grandi quantità di codice.

Tale idioma consente anche di velocizzare alcune operazioni, come lo swap tra due oggetti, ma in generale rallenta gli accessi ai dati dell'oggetto a causa del livello di indirezione, e provoca un'allocazione aggiuntiva per ogni creazione e copia di tale oggetto. Quindi non dovrebbe essere usato per classi le cui funzioni membro pubbliche sono chiamate frequentemente.

Iteratori e oggetti-funzione

modifica

Fa' in modo che gli oggetti iteratori o oggetti-funzione siano piccolissimi e che non allochino memoria dinamica.

Gli algoritmi di STL passano tali oggetti per valore. Pertanto, se la loro copia non è estremamente efficiente, gli algoritmi STL vengono rallentati.