Prototipi? Si, grazie!
considerazioni sull’uso dei prototipi in C (e C++)
Dante: Dunque un modo per aprirla è quello della dinamite. Sistema che usava il famoso fu Cimin. Tiberio: Fu Chi Min? Chi è, un cinese? Dante: Ma che cinese! Veneziano era! “Fu” sarebbe che morì, Cimin è il cognome, no?!.
Questo surreale dialogo tra Dante Cruciani (Totò) e Tiberio Braschi (Marcello Mastroianni) è tratto dal bellissimo I soliti ignoti, che è considerato (a ragione) uno dei prototipi della grande Commedia all’italiana. Ma cos’è un prototipo? Uhm… “Primo esemplare, modello originale di una serie di realizzazioni successive…” [Cit. Treccani]. Ecco, oggi parleremo dei Prototipi di Funzione nel linguaggio C (con qualche incursione nel C++) che sono molto somiglianti alla definizione del dizionario.
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 incontrato anche colleghi programmatori 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/C11. Se non altrimenti specificato tutte le prossime affermazioni/considerazioni si riferiranno alle versioni più recenti, C99 e C11 (che non hanno, tra di loro, differenze significative su questo argomento).
Veniamo al dunque: nel C i prototipi non sono obbligatori. La confusione su questo fatto 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/C11):
// dichiarazione di funzione (valida ma sconsigliata, perché ambigua e obsoleta) int myFunc(); // dichiarazione di funzione con prototipo int myFunc(int val): // definizione di funzione con prototipo int myFunc(int val) { int retval; // faccio cose ... return retval; }
L’ordine nell’esempio descritto, come evidente, non è casuale: la dichiarazione è il caso elementare, mentre il prototipo contiene implicitamente una dichiarazione e, infine, la definizione contiene implicitamente un prototipo (e quindi anche una dichiarazione). E, dato che ci siamo, aggiungiamo, per completezza, le sintassi di definizione permesse ma troppo old-fashioned, e le sintassi vietate in C99/C11:
// definizione con dichiarazione "old style" senza prototipo int myFunc(val) int val; { // faccio cose ... return 0; } // definizione implicitamente dichiarata (non permessa in C99/C11): // equivale a "int myFunc(int val)" myFunc(val) { // faccio cose ... return 0; }
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 retro-compatibilità 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 retro-compatibilità rimaneva, però, possibile scrivere nuovo codice con la sintassi antica, ed, 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 esempio di struttura elementare con tre file:
- un header-file che contiene i prototipi globali.
- un library-file che include l’header del punto 1 e contiene le definizioni (con prototipo) delle funzioni prototipate nel header-file.
- un implementation-file che include l’header del punto 1, e usa le funzioni prototipate nel header-file. L’implementation-file contiene, ovviamente, anche i prototipi delle eventuali funzioni locali e le relative definizioni (con prototipo).
E con questo sarebbe tutto, anche se possiamo aggiungere una interessante curiosità un po’ OT, ma che merita un approfondimento per evitare equivoci. Vediamo di cosa stiamo parlando:
// due dichiarazioni in C int myFunc1(); // funzione con numero arbitrario di argomenti int myFunc2(void); // funzione con 0 argomenti // due dichiarazioni in C++ int myFunc1(); // funzione con 0 argomenti int myFunc2(void); // funzione con 0 argomenti
Avete letto i commenti? Attenzione, quindi! In C int myFunc1() e int myFunc2(void) sono funzioni diverse, mentre in C++ significano la stessa cosa. Quindi occhio a non fare (in C) stranezze tipo queste:
// definizione con dichiarazione "old style" senza prototipo int myFunc1() { // faccio cose ... return 0; } // definizione di funzione con prototipo int myFunc2(void) { // faccio cose ... return 0; } // funzione main int main() { ... myFunc1(1, 2); // NOK: "undefined behaviour" e nessun warning/errore in compilazione myFunc2(1, 2); // OK: errore in compilazione per uso improprio della funzione ... return 0; }
Quindi, usando impropriamente una funzione con dichiarazione old-style (come la myFunc1() qui sopra) si produce un undefined behaviour (come ci conferma lo standard del C99) che si potrebbe trasformare in qualche mal di testa…
Ecco, quando passate frequentemente da C a C++ (e viceversa) ricordatevi di queste cose (e di alcune altre, ma questa è un altra storia…). Comunque, una cosa è sicura: le dichiarazioni old-style sono state mantenute per retro-compatibilità, ma non usatele mai, per favore!
Ciao, e al prossimo post!