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 20 gennaio 2013

Prototipi? Si grazie!
come usare i Prototipi in C

Dopo i bagordi delle feste di fine anno (e, magari, qualche chiletto accumulato da smaltire), meglio cominciare con un argomento leggero leggero: i Prototipi di Funzione. Leggero, ma non troppo.

Dopo una rapida ispezione in rete ho notato una certa confusione sull'argomento. Prototipi obbligatori, forse consigliati, a volte sconosciuti... ho notato informazioni fuorvianti perfino in dispense universitarie (ahi, ahi). Tra l'altro, nei miei trascorsi, ho parlato con vari colleghi C-Programmers che non avevano le idee chiare sull'argomento. Beh, allora è giunta l'ora di fare chiarezza!

Partiamo dai dati di fatto, lasciando alla seconda parte del post le considerazioni tecniche/filosofiche sull'argomento. Mi raccomando di prestare attenzione, nel seguito del testo, ad alcune parole chiave che useremo e cercheremo di illustrare: prototipo, dichiarazione e definizione. E, faremo riferimento anche alle varie versioni del C che ci hanno accompagnato fino ad oggi, che in ordine di tempo sono: K&R C, ANSI C (C89/C90) e C99 (ci sarebbe anche il C11, ma non è significativo per questo post). Se non altrimenti specificato tutte le prossime affermazioni/considerazioni si riferiranno al C attuale, il C99.

Veniamo al dunque: nel C i prototipi non sono obbligatori. La confusione su quest'argomento deriva dalla doppia personalità che hanno molti programmatori C (incluso il sottoscritto) che devono, spesso, districarsi tra C e C++ facendo, a volte, un po' di confusione: i prototipi sono obbligatori nel C++, per motivi strettamente collegati ad alcune funzionalità del linguaggio (vi suona il Function Overloading?).

Nel C, invece, è obbligatoria la dichiarazione di una funzione.

Facciamo, allora, un esempio sulle parole chiave dichiarazione, prototipo e definizione, usando solo una sintassi di tipo moderno (ANSI C o C99):
// dichiarazione di funzione
int myFunc();

// dichiarazione di funzione con prototipo
int myFunc(int val):

// definizione di funzione con prototipo
int myFunc(int val)
{
   if (val > 5)
       return val;
   else
       return val * 2;
}
L'ordine nell'esempio descritto, come evidente, non è casuale: la dichiarazione è il caso basico; il prototipo contiene implicitamente una dichiarazione, e, infine, la definizione contiene implicitamente un prototipo (e quindi anche una dichiarazione). Come detto, nell'esempio ho omesso, per non complicare inutilmente la descrizione, le sintassi permesse ma troppo old-fashioned, o le sintassi vietate dal C99.

Prima di passare alla parte filosofica, facciamo una breve analisi storica: nel K&R C non c'era l'obbligo di dichiarazione delle funzioni, quindi non c'era nessun controllo a compile-time sul valore di ritorno e, ancor meno, sulla coerenza dei parametri passati: in mancanza della dichiarazione il compilatore applicava un comportamento di default e assumeva che la funzione ritornava un int. Per i parametri si applicava la default argument promotion: gli interi venivano promossi a int, e i float erano promossi a double.

Con l'avvento del ANSI C (o C89/C90), sono arrivati i prototipi, però è stata mantenuta la retrocompatibilità con la vecchia sintassi (per non obbligare a sistemare milioni di linee di codice funzionante). Con questa novità era, finalmente, possibile controllare a compile-time la correttezza d'uso delle funzioni, sia sui parametri che sui valori di ritorno. A causa della retrocompatibilità rimaneva, però, possibile scrivere nuovo codice con la sintassi antica, e, inoltre, rimaneva valido il concetto del default return value in assenza di dichiarazione.

Con il C99 si è fatto un ulteriore passo in avanti: va bene la ricerca della compatibilità con il codice pre-esistente, ma il valore di ritorno di default era una falla troppo grande nella solidità del linguaggio, per cui si è introdotta la dichiarazione obbligatoria, come indicato all'inizio del post (aggiungo che si è anche reso obbligatorio l'uso dei prototipi negli standard headers del linguaggio, ma questa è un altra storia...).

E ora, dopo avere descritto quello che lo standard ci obbliga e/o permette di fare, veniamo, finalmente, a ciò che è meglio fare: secondo me un buon programmatore usa i prototipi (quindi, presumo, per la proprietà transitiva chi non usa i prototipi non è un buon programmatore. Ho detto presumo, quindi se qualcuno si è offeso non se la prenda con me, se la prenda con la proprietà transitiva). E perché consiglio così caldamente l'uso dei prototipi? Beh, il C é un linguaggio tipizzato, per cui è così evidente l'aiuto che questo meccanismo ci può dare per produrre codice senza errori di tipo, migliorando al tempo stesso leggibilità e manutenibilità, che non c'è neanche bisogno di spiegarlo!

E, per aggiungere un tocco di radicalità che non guasta mai, aggiungo che, per le suddette questioni di leggibilità e manutenibilità del software, non è conveniente affidarsi al fatto che usando definizioni con prototipo (vedi esempio sopra), e scrivendo il codice nel giusto ordine (cioè usando una funzione solo dopo la sua definizione), non è necessario scrivere dei veri e propri prototipi. Non siate pigri nelle cose utili, per favore!

E come deve essere strutturato un buon codice rispetto a quanto detto sopra? Vediamo un breve esempio con tre file: un header, un file con funzioni, e un file che le usa:

Questo è l'Header file:
/* myfuncs.h
 */
// prototipi globali
char *myFunc1(char *dest, const char *src);
char *myFunc2(char *dest, const char *src);
Ecco il file con le funzioni:
/* myfuncs.c
 */
#include "myfuncs.h"

// myFunc1()
char *myFunc1(char *dest, const char *src)
{
    ...
}

// myFunc2()
char *myFunc2(char *dest, const char *src)
{
    ...
}
E, infine, il file utilizzatore:
/* usefuncs.c
 */
#include "myfuncs.h"

// prototipi locali
static int useFuncs(void);
static int anotherFunc(void);

// anotherFunc()
static int anotherFunc(void)
{
    ...
    int res = useFuncs();
    ...
}

// useFuncs()
static int useFuncs(void)
{
    ...
    char *p1 = myFunc1(dest, src);
    char *p2 = myFunc2(dest, src);
    ...
}
E ho detto tutto!

Ciao e al prossimo post.

giovedì 20 dicembre 2012

La maledizione della Callback di giada
come scrivere una Callback in C

Oggi parleremo di un argomento un po' particolare, le funzioni callback. Perché particolare? Beh, tanto per cominciare, sulla bibbia del C (il K&R) non se ne parla mai, per cui un C-ista radicale potrebbe anche affermare che "le callback non esistono!". In realtà nel K&R si parla ampliamene degli zii delle callback, e cioè i puntatori a funzione (di cui le callback sono un caso particolare): quindi le callback esistono.

Non ho intenzione di scrivere un trattato di uso delle callback (sento già i vostri sospiri di sollievo), né di spiegare come, quando, perché e (soprattutto) se usarle: in rete ci sono molte fonti interessanti e ben scritte. Eventualmente sarebbe una buona cosa scrivere un post sugli zii, ma, visto l'argomento ostico e il probabile mal di stomaco che mi verrebbe durante la scrittura, lo rimando a data futura (tanto per intenderci: perfino Kernighan & Ritchie parlando dei Pointers to Functions hanno intitolato il capitolo 5.12 del K&R "Complicated Declarations", e se erano complicate per loro...).

Di cosa parleremo allora? Beh, io (come molti altri, immagino) ho usato varie volte le callback e ho letto codice che le usava (codice che era, il più delle volte, illeggibile e di difficile interpretazione, in rapporto direttamente proporzionale alla frequenza d'uso delle callback, sigh). Ebbene, fino al giorno in cui ho scritto una implementazione completa (e cioè ho scritto, oltre alla callback, anche la funzione a cui passarla) non mi sono reso conto di alcuni dettagli nascosti. Ovviamente, se per voi le callback non hanno dettagli nascosti, potete saltare la lettura del seguito e arrivederci al prossimo post.

Siete ancora qua? Ok, prima di cominciare vediamo una definizione delle callback presa pari pari dalla Wikipedia: "una funzione che viene richiamata da un'altra funzione" e aggiungo io: "e, spesso e/o normalmente, la funzione chiamante è una funzione di libreria". L'esempio più classico e familiare che si cita in letteratura è relativo all'uso della qsort():
// funzione callback di comparazione per la qsort()
static int cbCmpFunc(const void *elem_a, const void *elem_b)
{
    return *(int*)elem_a > *(int*)elem_b;
}

// main
int main(void)
{
    int array[] = {34,12,32,9,10,72,82,23,14,7,94};
    int nelems = sizeof(array) / sizeof(int);

    // eseguo sort array con qsort()
    qsort(array, nelems, sizeof(int), cbCmpFunc);

    // stampo risultati
    int i;
    for (i = 0; i < nelems; i++)
        printf("%d - ", array[i]);
    printf("\n");

    // esco
    return 0;
}
La qsort() è una funzione della libc che implementa l'algoritmo di ordinamento quicksort, e necessita di una funzione di callback che gli specifichi il tipo di ordinamento voluto. Siamo, quindi, in un caso classico: qsort() è una funzione di libreria, e noi, localmente, la usiamo passandogli una callback che, nel nostro esempio, serve per ordinare in modo ascendente i numeri dell'array.

E ora veniamo al dunque: in un altra epoca, quando non era facile come ora reperire documentazione ed esempi, mi era capitato di leggere e scrivere codice come quello mostrato sopra e mi domandavo (magari solo inconsciamente): "ma da dove saltano fuori i due parametri elem_a e elem_b della cbCmpFunc()?" e ancora: "Se io chiamo la callback e non gli passo esplicitamente parametri, come funziona il tutto?" Beh, come scoprii dopo, stavo ragionando rovesciando il rapporto causa-effetto: non ero io che chiamavo la callback, era la qsort() che la chiamava! Ok, mi vergogno un po' a raccontare una scoperta così lapalissiana, ma, effettivamente, lo compresi a fondo solo il giorno in cui ebbi la necessità di scrivere una funzione di libreria che usava una callback. Certo, ora con tutte le informazioni in rete è molto più facile...

Quindi, per chiarire, vediamo un esempio completo (N.B. l'esempio si potrebbe scrivere tutto in un file, ma, come evidenziato negli opportuni commenti, dovrebbe essere diviso su tre file in un progetto reale):
/* parte che dovrebbe essere nel file mysort.h
*/
// prototipi per mySort()
typedef int (*mycallback)(int, int);
void mySort(int *array, int nelems, mycallback cmpFunc);

/* parte che dovrebbe essere nel file mysort.c
 */
// mySort() - funzione di sort che usa l'algoritmo bubblesort
void mySort(int *array, int nelems, mycallback cmpFunc)
{
    // loop su tutti gli elementi di array
    while (nelems > 0) {
        // loop interno con lunghezza calante
        int i;
        for (i = 0; i < (nelems -1); i++) {
            // eseguo callback di comparazione
            if (cmpFunc(array[i], array[i + 1])) {
                // eseguo swap di array[i] e array[i+1]
                int temp = array[i];
                array[i] = array[i + 1];
                array[i + 1] = temp;
            }
        }

        // decremento nelems
        nelems--;
    }
}

/* parte che dovrebbe essere nel file mymain.c
*/
// cbCmpFunc() - funzione di comparazione
static int cbCmpFunc(int elem_a, int elem_b)
{
    return elem_a > elem_b;
}

// main
int main(void)
{
    int array[] = {34,12,32,9,10,72,82,23,14,7,94};
    int nelems = sizeof(array) / sizeof(int);

    // eseguo sort array
    mySort(array, nelems, cbCmpFunc);

    // stampo risultati
    int i;
    for (i = 0; i < nelems; i++)
        printf("%d - ", array[i]);
    printf("\n");

    // esco
    return 0;
}
Come vedete è molto simile all'esempio della qsort(), però, invece di usare una funzione della libc, usa una funzione di ordinamento scritta ad-hoc, la mySort(). Per quest'esempio ho usato, per non complicare troppo il codice, un algoritmo di tipo bubblesort, che è (lo so) un po' una schifezza, ma per fare un test semplice basta e avanza. Come noterete è la mySort() che si occupa di scrivere gli argomenti per la callback, processando opportunamente gli altri parametri che le vengono passati (array e nelems), e così, magicamente, appaiono dei valori in elem_a ed elem_b.

Che ve ne sembra? Si, un po' lapalissiano e ingenuo (forse), ma alzi la mano chi non ha mai avuto dubbi nel sua storia di programmatore (uhmm... vedo poche mani alzate).  E se questo post è servito a ampliare le conoscenze anche a solo uno dei miei lettori sono stra-contento così. E per gli altri, quelli che sapevano già tutto delle callback: perché siete arrivati ugualmente fino in fondo al post ?

Ciao e al prossimo post.