Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

domenica 23 settembre 2012

Variabili globali? No, Grazie
come usare le variabili globali nel C

Oggi cercheremo di demolire un altro dei capisaldi della non-programmazione: la variabile globale. Intendiamoci: le variabili globali (come anche il goto, per esempio) fanno parte del linguaggio, quindi esistono: a volte è possibile e/o necessario usarle. Però, esattamente come il goto, è quasi sempre possibile farne a meno, con grandi benefici di stile (leggibilità e manutenibilità, soprattutto), e funzionalità (meno bugs): chiamalo poco.

I punti critici sono moltissimi, ma, visto che non voglio scrivere né un poema, né un libro sull'argomento, ne ho isolati alcuni. Vediamo:

1) le variabili globali non sono thread-safe: nella programmazione multithreading l'uso delle globali può generare problemi di malfunzionamento subdoli e difficili da trovare. Il caso classico sono le istruzioni di lettura/scrittura di una globale fuori da una zona critica, ossia (per esempio) senza la protezione di un mutex: in un grande progetto è sufficiente una (una sola!) dimenticanza, di questo tipo per creare tanti di quei mal di testa che ti passerà la voglia di usare le globali per il resto della tua vita di programmatore. Provare per credere. A questo punto uno potrebbe obbiettare: "ma io scrivo programmi normali, non multithreading". Va bene: premettendo (e chiarendo nel prossimo paragrafo) che un programma "normale" può essere considerato un programma multi-thread con un solo thread, ringrazio per l'obiezione, che mi serve giusto di spunto per illustrare il punto 2:

2) le variabili globali violano il principio di manutenzione/riutilizzazione del Software: quando si scrive del codice professionalmente bisogna sempre pensare al lavoro in team, e quindi alle operazioni di manutenzione che potrebbero essere svolte da altri (e, a volte) dopo molto tempo: evidentemente un codice, vasto e pieno di globali, è difficile da manutenere come un codice No Comment (ricordate ?), perché la storia di una globale è poco comprensibile, potrebbe essere toccata in molti posti diversi da molte funzioni diverse, e, se per capire un pezzetto di codice bisogna aprire alcune decine di file... avete vinto un altro bel mal di testa! E non ne parliamo di riutilizzare del codice pieno di globali per un altro progetto: se non l'avete mai fatto provate almeno a immaginarvi la difficoltà. E non solo: torniamo al punto 1 (thread-safe): chi mi dice che del codice "normale" non lo debba riutilizzare (un giorno) in un progetto multithread ? Se il codice è thread-safe si può fare agevolmente, ma se ci sono delle globali di mezzo... beh, buon lavoro (e buona fortuna).

3) le variabili globali aumentano la difficoltà del debug e moltiplicano la probabilità di errori di programmazione: per quanto riguarda il debug vi rimando al punto 2: se il valore di una variabile è difficile da seguire a livello di manutenzione lo sarà anche a livello di debug. E ci saranno anche più malfunzionamenti da debuggare (fantastico!), perché, oltre a tutti i possibili errori di codificazione ci aggiungiamo anche quelli di scope: provate questo codice:
int my_var = 0;

void incrementaMyVar()
{
    my_var += 5;
}

int main(int argc, char **argv)
{
    // faccio mille cose...
    // ...

    // incremento my_var
    incrementaMyVar();

    // faccio altre mille cose...
    // ...

    // definisco una "nuova" variabile my var e la uso
    int my_var = 2;        // ah, ah, ah: ridefinizione!
    // ...

    // faccio ancora mille cose...
    // ...

    // incremento e test my_var (oops! quale my_var?)
    incrementaMyVar();
    if (my_var == 2)
        formatMyHardDisk();    // uh, uh, uh: era quella sbagliata!
    // ...

    return 0;
}
Quello mostrato sopra era un problema di scope con ridefinizione locale (accidentale) di una globale. Il codice che segue è ancora più semplice, mostra una svista su un dettaglio importante:
int my_var = 0;

void faccioMilleCose()
{
    // faccio mille cose...
    // ...

    // incremento my_var (sepolto tra mille istruzioni!)
    my_var += 5;

    // faccio altre mille cose...
    // ...
}

int main(int argc, char **argv)
{
    // faccio un po' di cose...
    // ...

    // chiamo faccioMilleCose()
    faccioMilleCose();    // oops! ho incrementato my_var per sbaglio

    // faccio altre cose...
    // ...

    // test my_var
    if (my_var == 5)
        formatMyHardDisk();    // uh, uh, uh: ho sbagliato qualcosa?
    // ...

    return 0;
}
bella storia, eh ?

4) le variabili globali violano il principio di incapsulamento delle variabili: beh, questo non c'è nemmeno di bisogno di spiegarlo, una globale è tutto meno che incapsulata... oops, ma questo è OOP, quindi esula un po' l'argomento del blog: scusatemi, volevo strafare. Beh, visto che siamo in argomento OOP, e quindi C++, cito volentieri il grande M.Cline che nelle su C++FAQ dice (traduco, eh):
Il nome di una variabile globale deve iniziare con //.

Ecco il modo ideale di dichiarare una variabile globale:

// int xyz; <-la cosa che rende ideale questa globale è l'iniziale //

Ecco il modo ideale di usare una variabile globale:

void mycode()
{
  ...
  // fai_qualcosa_con(xyz);  <-idem come sopra
  ...
}

OK, questo è un gioco. Una specie di. La verità è che ci sono casi in
cui le variabili globali sono meno peggio delle alternative - quando
le globali sono il minore dei mali. Ma loro sono sempre malvage. 
Quindi lavatevi le mani dopo averle usate. Due volte.
Sante parole.

Comunque, se pensate che tutto questo sia solo farina del mio sacco provate a chiedere al nostro amico Google: global variable are evil? e vedrete che valanga di risultati viene fuori. Se vi interessa una delle pagine più interessanti la trovate qui.

Penso che quanto detto sia sufficiente. Evidentemente io sono un po' prevenuto perchè ho lavorato a lungo su software multithreading (e ho ancora un po' di mal di testa...), però vi chiedo di fidarvi e di propagare il messaggio il più possibile. Si, lo so, dire a un programmatore inesperto (o a un programmatore stanco) "non usare le variabili globali" è come dire a un bambino "non mangiare troppe caramelle": beh, bisogna farlo. La salute prima di tutto.

Ciao e al prossimo post.

domenica 9 settembre 2012

No Comment 2 - La vendetta
come scrivere i commenti nel C - pt.2

Oggi è venuto il momento di completare il discorso del No Comment che trovate qui. Avevamo, per così dire, parlato a livello micro (i commenti ai blocchi di codice), ma abbiamo tralasciato il livello macro, e cioè il contenitore stesso (il file) e i grossi blocchi di codice che sono le funzioni. Ditemi la verità: se lavorate (o avete lavorato) in team, quante volte avete aperto un file scritto da un collega (appartenente alla religione No Comment, devoto e praticante) e avete scoperto che l'unico indizio sul codice contenuto era il nome del file stesso ? Poi, visto che per la Legge di Murphy, l'esimio collega era in ferie (giuste e meritate), avete impiegato un giorno intero solo per capire a cosa servisse quel file, e, solo a quel punto, avete incominciato a decifrare il codice grazie alla Stele di Rosetta.

Un file ha bisogno di un header, e le funzioni (almeno quelle importanti, che di solito sono quelle globali) hanno bisogno di una intestazione fatta come si deve (comunque anche le funzioni statiche si meritano un minimo di presentazione). Spero che il messaggio sia chiaro.

Va bene, e allora come lo facciamo un header ? Premettendo che le varianti possibili sono infinite, direi che, per cominciare, anche qualcosa semplice semplice, come questa, potrebbe andare:
/*!
 *  FILE
 *     getComments.c - estrazione commenti da un sorgente C
 *  PROJECT
 *     myDoc - generazione automatica documentazione
 *  FUNCTIONS
 *     globali:
 *        getComments()
 *     statiche:
 *        formatLine()
 *  AUTHOR
 *     A.Abate
 *  OPERATING SYSTEM
 *     Linux (all versions)
 *  COMPILER
 *     GNU gcc
 *  RELEASE
 *     1.0 (Settembre 2012)
 */ 

Evidentemente si può abbondare più o meno con le descrizioni. Per esempio, se il file contiene il codice di un protocollo di comunicazione, si potrebbe aggiungere una descrizione a blocchi (un disegno in modo text) con le entità implicate e i flussi di comunicazione dei messaggi.

Per quanto riguarda le funzioni importanti, si può usare la classica sintassi delle Man Pages di Unix (e Linux), e quindi fare qualcosa del genere:
/*!
 *  NAME
 *     getComments - esamina un sorgente C per estrarre i commenti
 *  SYNOPSIS
 *     int getComments(
 *        char *inpath,
 *        char *oupath)
 *  DESCRIPTION
 *     Estrae i commenti da un sorgente C con pathname <inpath> e li
 *     scrive in un file di documentazione con pathname <outpath>.
 *  RETURN VALUE
 *     0 a operazione effettuata. Un valore negativo in caso di
 *     errore.
 */ 
E, anche qui, si può abbondare più o meno con le descrizioni, aggiungendo, per esempio, dettagli su tutti i codici di ritorno, e, se necessario, inserendo un ulteriore appartato EXAMPLES, con brevi esempi d'uso.

Come i più osservatori avranno notato, la prima riga dei due esempi qui sopra comincia con "/*!", ossia contiene un simbolo di riconoscimento per un eventuale tool di auto-generazione della  documentazione (tipo Doxygen). E, sempre i più osservatori, avranno notato che anche il contenuto dei due esempi parla di auto-documentazione. Il fatto è che ho pensato di proporre un caso reale, e cosa c'è di meglio che scrivere, ad-hoc, un semplicissimo tool di documentazione che ho scritto e testato in pochissimo tempo (è veramente molto semplice, ma si può perfezionare).

Beh, il mini-tool sarebbe (quasi) tutto qua:
int getComments(
    char *inpath,
    char *oupath)
{
    char *line = NULL;
    size_t len = 0;
    int retcode;
    bool start_comment = false;

    // apro il file di input
    FILE *fpin;
    if (fpin = fopen(inpath, "r")) {
        // apro il file di output
        FILE *fpout;
        if (fpout = fopen(oupath, "w")) {
            // leggo il file di input
            while (getline(&line, &len, fpin) != -1) {
                // test fine commenti per reset flag start_comment
                if (strstr(line, "*/") && start_comment) {
                    start_comment = false;
                    fprintf (fpout, "\n\n\n");
                }

                // test flag start_comment
                if (start_comment) {
                    // formatto e scrivo linea su file output
                    fprintf(fpout, "%s", formatLine(line));
                }

                // test inizio commenti per set flag start_comment
                if (strstr(line, "/*!"))
                    start_comment = true;
            }

            // libera risorse e set retcode=OK
            fclose(fpout);
            free(line);
            retcode = 0;
        }
        else
            retcode = -2;    // fopen err: set retcode=err

        fclose(fpin);    // libera risorse
    }
    else
        retcode = -1;    // fopen err: set retcode=err

    // esce con il retcode settato
    return retcode;
}
Direi che il codice è così intuitivo (e commentato) che non c'è molto da aggiungere: si cerca il codice di riferimento (/*!) e si copia in un file di documentazione tutto quello che lo segue fino a fine commento. La funzione formatLine() (che ho omesso) può non fare nulla (ossia restituire direttamente la linea passata), o fare qualche elaborazione più o meno sofisticata (come minimo raccomando di togliere gli inizi linea del tipo " *"). Io, per testare l'applicazione, ho scritto un main() che ricorre tutti i sorgenti di una directory e scrive i risultati in un altra directory (e, direi, funziona bene).

Quella presentata è una alternativa rapida ai vari tool tipo Doxigen, che sono, ovviamente, raccomandabili per grossi progetti (ma anche l'alternativa artigianale descritta non è male).

Chiudo con un messaggio per quelli che pensano (come già fatto notare qui) che scrivere i commenti (e, a maggior ragione, headers e descrizioni di funzioni) sia tempo perso che allunga i tempi di sviluppo. Bisogna avere larghezza di vedute, ragionare a lunga scadenza: anche se aumento del 10% i tempi di sviluppo (e documentare non richiede molto tempo, specie se lo fai mentre scrivi il codice), verrà il giorno in cui si recupererà con gli interessi il tempo perso in partenza, per esempio alla prima occasione di manutenzione o modifica (provate a farlo su un codice No Comment, poi mi raccontate...). E il tempo è denaro.

Ciao e al prossimo post.