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 17 febbraio 2013

C orientato agli oggetti! C orientato agli oggetti?
come usare la OOP in C

L'argomento del post di oggi è un ossimoro. Il C è un linguaggio decisamente procedurale (oserei dire che è IL linguaggio procedurale), per cui pensare di fare Object Oriented Programming con il C non ha molto senso... però è possibile, e senza scomodare il figlioletto C++! Presenterò un esempio semplice semplice in C++ (ma proprio semplice), e cercheremo di vedere il suo equivalente in C. Per concludere, vi anticipo, scriverò alcune note filosofiche su C, C++ e OOP. Poi, magari, mentre scrivo il post mi stufo e decido di non scriverle, oppure me ne dimenticherò e non le scriverò (soluzione diplomatica: se scrivo ciò che ho in mente adesso qualcuno potrebbe sentirsi "toccato"...).

Condizione necessaria (e sufficiente), per continuare a leggere il post, è il possesso di alcune nozioni di C++ e OOP: se non le avete iscrivetevi a un corso accelerato di C++ o leggetevi rapidamente un manuale... comunque, data la semplicità dell'esempio, non tentate diventare super-esperti del C++ per finire di leggere il post. Anche perché, in tal caso (super-esperti!), vi potrebbe servire così tanto tempo che potrebbe non esistere più né questo blog né internet... ("life is too long to be good at C++", Erik Naggum).

Veniamo al dunque: scriviamo una classe elementare in C++ che implementa un contatore. Scriveremo, secondo la prassi classica, su tre file: header, implementazione e uso. Vediamo l'header counter.hpp:
/* counter.hpp - header classe CCounter
 */
class CCounter {
private:
    // attributi privati
    int value;

public:
    // metodi pubblici
    CCounter();          // costruttore
    void incValue(int n);
    int getValue();
};
Come promesso è una classe semplicissima, con un attributo privato, due metodi di lettura e scrittura e un costruttore esplicito. Passiamo al file di implementazione, counter.cpp:
/* counter.cpp - implementazione classe CCounter
 */ 
#include "counter.hpp"

// CCounter() - costruttore della classe CCounter
CCounter::CCounter()
{
    value = 0;
}

// incValue() - metodo di incremento attributo value
void CCounter::incValue(int n)
{
    value += n;
}

// getValue() - metodo di lettura attributo value
int CCounter::getValue()
{
    return value;
}
Evidentemente non c'è neanche da spiegare come funzionano i metodi appena descritti (e, se state ancora leggendo, dovreste avere già fatto, con successo, il corso accelerato di C++...). Per ultimo vediamo il file di uso della classe, countermain.cpp:
/* countermain.cpp - esempio d'uso della classe CCounter
 */
#include <iostream>
#include "counter.hpp"

using namespace std;

main()
{
    CCounter cnt_a;
    CCounter cnt_b;

    // leggo valori
    cout << "cnt_a value = " << cnt_a.getValue() << endl;
    cout << "cnt_b value = " << cnt_b.getValue() << endl;

    // incremento valori
    cnt_a.incValue(5);
    cnt_b.incValue(7);

    // leggo valori
    cout << "cnt_a value = " << cnt_a.getValue() << endl;
    cout << "cnt_b value = " << cnt_b.getValue() << endl;
}
Ok, l'uso della classe è un po' stupido, ma per quest'esempio è più che sufficiente. Instanziamo due oggetti della classe CCounter, cnt_a e cnt_b, stampiamo i valori di value per entrambi gli oggetti, incrementiamo e ristampiamo: se compilate ed eseguite il programma l'uscita sarà:
cnt_a value = 0
cnt_b value = 0
cnt_a value = 5
cnt_b value = 7
Come vedete, per la magia della OOP e del C++ i due oggetti hanno vita propria, e, alla fine, il campo value ha valore diverso nei due oggetti (merito del mitico puntatore this... ma questo dovreste già saperlo e questo non è un corso di C++, quindi non lo spiegherò).

Tutto questo si può fare anche in C? Si! Beh, non proprio identico, ma quasi. Useremo la stessa struttura a tre file. Cominciamo con l'header ccounter.h:
/* ccounter.h - header classe CCounter in C
 */
typedef struct _ccounter {
    // attributi
    int value;

    // metodi
    void (*construct)(struct _ccounter *this);       // costruttore
    void (*incValue)(struct _ccounter *this, int n);
    int (*getValue)(struct _ccounter *this);
} CCounter;
Somiglia molto a ccounter.hpp, no? Ovviamente non ci sono le parole magiche public e private, e i metodi sono degli splendidi (?) puntatori a funzione, comunque la somiglianza è evidente. passiamo alla implementazione in ccounter.c:
/* ccounter.c - implementazione classe CCounter in C
 */
#include "ccounter.h"

// incValue() - metodo di incremento attributo value
static void incValue(CCounter *this, int n)
{
    this->value += n;
}

// getValue() - metodo di lettura attributo value
static int getValue(CCounter *this)
{
    return this->value;
}

// construct() - costruttore della classe CCounter
static void construct(CCounter *this)
{
    this->incValue = &incValue;
    this->getValue = &getValue;
    this->value = 0;
}

// pubConstruct() - costruttore pubblico della classe CCounter
void pubConstruct(CCounter *this)
{
    this->construct = &construct;
    this->construct(this);
}
Qui le differenze sono più evidenti, però la somiglianza strutturale è altrettanto chiara. Rispetto alla versione C++ dobbiamo fare in maniera esplicita ciò che il compilatore C++ fa implicitamente: a ogni metodo passiamo un puntatore all'oggetto chiamante (che strano, l'ho chiamato this, anche se non so perché, sarà qualcosa di inconscio), i metodi sono assegnati ai puntatori a funzione, e dobbiamo aggiungere un metodo in più, un costruttore pubblico, da chiamare dopo l'instanziazione dell'oggetto (anche questo il compilatore C++ lo fa implicitamente).

Ci resta solo da vedere il file d'uso, ccountermain.c:
/* ccountermain.c - esempio d'uso della classe CCounter in C
 */
#include <stdio.h>
#include "ccounter.h"

main()
{
    CCounter cnt_a;
    pubConstruct(&cnt_a);

    CCounter cnt_b;
    pubConstruct(&cnt_b);

    // leggo valori
    printf("cnt_a value = %d\n", cnt_a.getValue(&cnt_a));
    printf("cnt_b value = %d\n", cnt_b.getValue(&cnt_b));

    // incremento valori
    cnt_a.incValue(&cnt_a, 5);
    cnt_b.incValue(&cnt_b, 7);

    // leggo valori
    printf("cnt_a value = %d\n", cnt_a.getValue(&cnt_a));
    printf("cnt_b value = %d\n", cnt_b.getValue(&cnt_b));
}
Beh, questo è praticamente identico alla versione C++, salvo la chiamata esplicita al costruttore pubblico, e all'uso della printf() per la stampa dei risultati. Se compilate e eseguite, il risultato (giuro!) è identico a quello della versione C++.

Nel prossimo post, vi mostrerò come sia possibile implementare, in C, anche la ereditarietà delle classi (wow!). Adesso non ne ho voglia, e mi dilungherei troppo (non voglio annoiare nessuno). Anzi, come previsto all'inizio del post, non scriverò neanche le disquisizioni filosofiche... ma anche quelle sono solo rimandate al prossimo post.

Prima di lasciarci non posso evitare di scrivere una nota di riflessione (ma non perdeteci il sonno ripensandoci!): l'esempio appena mostrato serve a poco (anzi, a niente!). Però è una interessante dimostrazione di flessibilità e potenza del nostro amato C, e aggiungo, una dimostrazione reale delle origini del C++: forse non tutti sanno che, moooolti anni fa (anni '80) alcuni dei compilatori C++ disponibili sul mercato erano, in realtà, dei mega-preprocessori, che leggevano il codice C++, lo trasformavano in C, e lo compilavano con un normale compilatore C. Ecco, l'esempio qui sopra è (più o meno...) quello che facevano i compilatori C++ primordiali, quando il C++ era ancora solo un C a oggetti. Meditate gente, meditate...

Ciao e al prossimo post.

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.