Tuesday, 19 October 2010

La trappola del C++ (terza parte)


Nel procedente post ho mostrato come scrivere un programma in C++ che soddisfi il requisito della portabilità sia un compito più arduo di quanto si possa credere. Tuttavia molti programmi (come Blender 3D e Mozilla Firefox) sono stati portati con successo su diverse piattaforme.
Il segreto di questi programmi è relativamente semplice: i programmatori hanno costruito una solida e veloce base in C++ cui hanno agganciato l'interprete di un linguaggio di scripting (python nel caso di Blender 3D e XUL per tutti i prodotti di Mozilla). Una volta testato che la base C++ è robusta, tutto il resto è script. Se una funzione è troppo lenta la si riscrive in C++, ma tutto il resto rimane immutato e funzionerà su qualsiasi piattaforma. Se c'è un errore nella logica di programma probabilmente è all'interno di uno degli script: in questo caso è molto più comodo correggere uno script, piuttosto che ricompilare qualche mega di sorgente, non trovate? Lo stesso dicasi nel caso si voglia aggiungere una funzionalità: si scrive un file e lo si sposta nella cartella degli script.
Nel mondo dei videogame c'è un illustre esponente di questa tecnica: Quake 2, che John Carmack scrisse in puro C e che dotò di un motore di scripting scritto apposta per tutta la logica di gioco.
E nel free-software? Qualcuno conosce un altro programma che segua questa filosofia? Un applauso per chi ha detto EMACS.

fig 1
A questo punto dovrei parlare della gestione della memoria, croce e delizia del programmatore C.
Qua apro una parentesi di storia personale: spesso ho trovato persone che si son lanciate entusiaste nel famoso discorso "C++ è figo, Java è uno schifo". Di tutti questi, solo uno è riuscito a finire un progetto (circa 10.000 righe di codice) e ha passato le notti sul debugger per trovare le istruzioni delete che mandavano in crash il programma. Questo perché bisogna far ben attenzione a quando invocare una delete.
Prendete, ad esempio, il grafico riportato in Figura 1.
In questo quadro, la classe Camera ha richiesto a MediaCache un oggetto di tipo Image. Per comodità, di questo oggetto abbiamo passato il puntatore.
Immaginiamo, ora, che il programmatore diligente lanci una delete appena Camera ha terminato di usare l'oggetto ImageToDisplay. Il risultato è che anche l'elemento dell'array di MediaCache punterà a niente, con risultati disastrosi (Figura 2).

fig 2
Objective-C risolve il problema utilizzando oggetti che tengono il conto del numero referenze che puntano a essi utilizzando i metodi retain e release. E' molto bello come sistema, e vi consiglio di dare un'occhiata alla sezione sette del Cocoa Dev Central e alla Figura 3.

fig.3
In Objective-C, si va a liberare lo spazio solo
quando il contatore delle referenze va a zero
Certo, il caso del motore grafico 2D è estremamente semplice e si può facilmente capire cos'è che va storto. Ma provate a fare il debug in un contesto di più seimila righe di codice e perderete sicuramente qualche oretta nel capire come mai il programma da segmentation fault.
Alcuni programmatori C++, sapendo che avrebbero potuto dimenticare qualche delete (in programmi lunghi milioni di righe di codice è una svista inevitabile), risolvevano il problema con un'area tampone d'emergenza, ossia:

  1. All'inizio del programma, come prima cosa, si alloca 1 Megabyte di RAM a vuoto (puntato da char* help_buffer;)
  2. Ad ogni new, si va a verificare che il risultato sia diverso da NULL. In caso contrario, c'è stato un errore di memoria: probabilmente i memory leak hanno saturato la memoria e rendono impossibile qualsiasi altra operazione.
  3. Si dealloca lo spazio puntato da help_buffer. Si libera così abbastanza memoria per resettare tutto il programma e, nel peggiore dei casi, a riavviarlo.
La soluzione è brutta, è poco elegante, ma in contesti critici (server, per esempio) garantisce che il programma continui a girare ininterrottamente. Immaginate un server web che va si pianta una volta al giorno:.i sistemisti non vi ringrazieranno per la straordinaria velocità e vi malediranno per la scarsa affidabilità. Il debug, le limature e la raffinazione del programma si farà poi, la priorità è che il programma funziona ed è affidabile.

Se programmate in C++ non sottovalutate MAI il debugger: è il vostro migliore strumento e più fido alleato. Evitate di ricorrere sempre ai cout<< di variabili. Sono utili per avere il log di sistema e per aiutare voi e gli utenti a fare un bug report. Ma un buon debugger vi servirà a capire precisamente cos'è andato storto: una delete azzardata, una new andata male o una ricerca all'interno di un array vuoto vengono facilmente individuate da un debugger.

Conclusioni
Se state programmando in C++, senz'altro avrete bisogno di un debugger. Se state programmando in C++ fate molta attenzione alla gestione della memoria: individuare i memory leak e i crash dovuti a maldestre free è difficile. Se in fase di progetto vi accorgete che il programma inizia ad essere davvero complicato, valutate se è il caso di utilizzare un linguaggio di scripting al suo interno.
Nella prossima parte, concluderò questa serie di articoli rispondendo alla domanda «ma abbiamo ancora bisogno del C++?»
(continua nella domani nella quarta e ultima parte)
Post a Comment