Che tipacci

Oggi si parla di C++ e template, mettetevi l’elmetto.

Il C++ è un linguaggio tipato. Significa che se volete definire una variabile dovete sempre specificare il suo tipo. Questa cosa qui è abbastanza differente da linguaggi di scripting e interpretati che spesso deducono il “tipo di dato” della variabile al volo. Ad esempio, in Python potete scrivere:

a = 5
b = 7.1
print "risultato di a*b = ", a*b
print "risultato di a/2 = ", a/2

Il programmatore non specifica che cosa siano a e b. Interi o numeri con virgola? Si capisce solo dal valore che gli viene assegnato. In questo caso, a è un intero, e b è un floating point. Il risultato della moltiplicazione è 35.5, mentre il risultato di a/2 è 2 (e non 2.5 ).

I linguaggi dinamici come il Python sono molto comodi, ma se fate un errore ve ne accorgete solo a tempo di esecuzione. Ad esempio, magari volevate dire che a era un floating point: avreste dovuto scrivere

a=5.0

In questo caso il risultato della divisione veniva fuori 2.5. Ma se vi scordate il .0 finale, ve ne accorgete solo quando esegue il programma. Il C++ invece è un linguaggio tipato, quindi per fare la stessa cosa, dovete scrivere:

int a=5;
float b=7.1;
cout << "risultato di a*b" << a*b << endl;
cout << "risultato di a/2" << a/2 << endl;

Qui si vede chiaramente che a è una variabile che contiene esclusivamente numeri interi, mentre b contiene numeri in virgola mobile. In realtà, anche qui se sbagliate a settare il tipo ve ne accorgete solo a run-time. Però, concorderete (spero) che sia più facile scordarsi il .0 finale che sbagliare int per float.

Anche per le classi è gli oggetti è così, anzi peggio (o meglio, dipende). In Python, se definite una semplice classe, potete poi creare oggetti della classe e assegnarli a variabili senza alcun problema. Ad esempio:

class NameClass :
    def __init__(self, n) :
        self.nome = n

class ValueClass :
    def __init__(self, a) :
        self.valore = a

oggetto1 = NameClass("Giuseppe")
oggetto2 = ValueClass(10)
print "nome", oggetto1.nome
print "valore", oggetto2.valore

In questo caso, assumiamo che la classe NameClass abbia un campo nome che contenga un nome di persona; e la seconda classe ValueClass un campo valore che contiene un numero. Le variabili oggetto1 e oggetto2 “contengono” (sarebbe meglio dire puntano a) due oggetti di “tipo” NameClass e ValueClass, rispettivamente. Ma nessuno mi vieta di scrivere:

ogg3 = NameClass(10);
ogg4 = ValueClass("Riccardo")
print "nome", ogg3.nome
print "valore", ogg4.valore

Ora il campo nome contiene un numero, e il campo valore contiene una stringa e Python non si arrabbia. E come potrebbe? Non c’è scritto da nessuna parte che nome è una stringa e che valore è un intero.
Il C++ invece vi costringe a dichiarare tutto. L’esempio precedente diventa:

#include <iostream>
#include <string>
using namespace std;

class NameClass {
public:
    string nome;
    NameClass (const string &n) : nome(n) {}
};

class ValueClass {
public:
    int valore;
    ValueClass(int n) : valore(n) {}
};

int main() {
    NameClass oggetto1("Giuseppe");
    ValueClass oggetto2(10);
    cout << "nome   " << oggetto1.nome << endl;
    cout << "valore " << oggetto2.valore << endl;
}

Un po’ lunghetto, eh? Comunque, tutte le variabili hanno il loro tipo, e tutto funziona a modino. Però, se vi sbagliate e scrivete:

...
int main() {
    NameClass oggetto1(10);
    ValueClass oggetto2("Giuseppe");
    cout << "nome   " << oggetto1.nome << endl;
    cout << "valore " << oggetto2.valore << endl;
}

Il compilatore g++ vi risponde così:

myclass.cpp: In function ‘int main()’:
myclass.cpp:18: error: invalid conversion from ‘int’ to ‘const char*’
myclass.cpp:18: error:   initializing argument 1 of ‘std::basic_string::basic_string(const _CharT*, const _Alloc&) [with _CharT = char, _Traits = std::char_traits, _Alloc = std::allocator]’
myclass.cpp:19: error: invalid conversion from ‘const char*’ to ‘int’
myclass.cpp:19: error:   initializing argument 1 of ‘ValueClass::ValueClass(int)’

Che tradotto per i comuni mortali significa:

Ahò, ma me stai a cojonà?
Guarda che NameClass vole 'na stringa! E ValueClass vole un numero!

Insomma, il compilatore è come una maestrina (de Roma) che sta lì a bacchettarci sulle dita ogni volta che scriviamo una cavolata. Tutto deve tornare, stringhe con stringhe e numeri con numeri.

Pro e contro
I tifosi del Python diranno: ma che palle, che me ne frega? La risposta è: controllare errori stupidi e meno stupidi prima di andare in esecuzione è fondamentale per ridurre il numero di insettacci schifosi che infestano i programmi. Sarebbe un sogno se il compilatore potesse magicamente controllare il programma e trovare tutti i bug che il programmatore inserisce per distrazione, per insipienza, per malafede, etc. Purtroppo questo è e rimarra un sogno ancora molto a lungo: ma più il compilatore ci aiuta a toglierli, meglio è.
Purtroppo, è vero che questo richiede uno sforzo aggiuntivo non banale. Sforzo che viene ripagato, specialmente quando si programma in maniera professionale, ve lo assicuro.

I tifosi del C++, d’altro canto, penseranno che Python sia un linguaggio per sciatti programmatori nostalgici del basic. Niente di più falso. Python predilige la velocità di produzione del codice: poiché è più facile scrivere programmi in Python, un programma complesso viene realizzato in meno tempo e in maniera più efficiente, quindi risparmiando sul costo di sviluppo. Vi sembra poco? Non lo è affatto! E d’altro canto, Python è un linguaggio interpretato, e l’interprete non può certo perdere tempo a guardare prima tutto il programma alla ricerca di errori, come fa il compilatore.

Problemi con i tipi
Il problema è che alle volte, in C++, vorremmo scrivere una sola volta codice generico che sia indipendente dal tipo. L’esempio che faccio sempre a lezione è quello della funzione swap, per scambiare due interi. Eccola:

#include <iostream>
using namespace std;
void swap(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}
int main() {
    int x=10, y=20;
    swap(x,y);
    cout << "x =" <<  x << " y = " << y << endl;
}

La funzione swap prende come argomenti due riferimenti a interi (se non sapete cosa sono i riferimenti in C++, leggetevi questo(*)), e li scambia fra loro. Se eseguite il tutto, i valori di x e y risulteranno scambiati.
Ok, ma se volessi scrivere una funzione swap per floating point? Sarebbe praticamente uguale, con l’uso di float invece di int. E se volessi scrivere una swap per due oggetti? Idem con patate! Ecco una swap per la classe NameClass:

void swap(NameClass &a, NameClass &b) {
    NameClass tmp = a;
    a = b;
    b = tmp;
}

(l’esempio presuppone che sia possibile copiare oggetti di NameClass, ed in effetti è possibile con la classe NameClass … maggiori dettagli nelle prossime lezioni).
Si, però adesso ci siamo scocciati di scrivere sempre lo stesso codice. Un certo Don Roberts (citato da Martin Fowler) disse una volta: three strikes and you refactor.

La swap() è stata già scritta due volte, è quindi il momento di generalizzare. Il C++ mette a disposizione una cosa potentissima: i template (elmetto ben stretto, mi raccomando!). Ecco come si scrive una swap generica:

#include <iostream>
#include <string>
using namespace std;

class NameClass {
public:
    string nome;
    NameClass (const string &n) : nome(n) {}
};

template<class T>
void swap(T& a, T&b) {
 T tmp = a;
 a = b;
 b = tmp;
}

int main() {
 NameClass ogg1("Giuseppe");
 NameClass ogg2("Roberto");
 swap(ogg1, ogg2);

 int x = 10, y = 20;
 swap(x, y);
}

Il template è una specie di generatore automatico di codice. T è un generico tipo, e non stiamo a dire esattamente quale. Nel momento in cui invochiamo la swap(), il compilatore cerca di capire che tipo deve essere T: nel primo caso, poiché stiamo passando oggetti di tipo NameClass, T sarà appunto NameClass; nel secondo caso stiamo passando degli interi, e quindi T sarà int. Si dice che il tipo viene automaticamente dedotto dal compilatore. Inoltre, il compilatore controlla che tutto quello che abbiamo scritto nella funzione si possa effettivamente fare con il tipo T: nel nostro caso, deve essere possibile copiare e assegnare oggetti di tipo T, ed in effetti questo si può fare sia per T = int sia per T = NameClass.

Conclusioni (parziali)
Il C++ è un linguaggio tipato, e questo è un bene perché il compilatore così ci aiuta a trovare bug. I tipi però a volte sono una seccatura perché ci costringono a scrivere un sacco di codice, e noi vogliamo evitare la sindrome del tunnel carpale. Per questo motivo, il buon Bjarne Stroustrup che ha inventato il C++, ha pensato bene di inventare i template che ci risparmiano un sacco di seccature.

Nella prossima puntata scopriremo che

Il povero Bjarne Stroustrup aveva pensato i template come un meccanismo simile alle macro per fare cose tipo la swap, e chiuso lì. Se solo avesse immaginato il casino in cui si era ficcato… hell breaks loose!

(*) Lo so che wikipedia non è il massimo per queste cose, ma non ho roba migliore sotto mano al momento!

Posta un commento o usa questo indirizzo per il trackback.

Commenti

  • juhan  Il 2 aprile 2011 alle 18:35

    Bellissimo, solo una piccola avvertenza: il primo esempio in Python ha bisogno di una precisazione. È vero che 5 / 2 da 2 nella versione 2.x ma dalla 3.0 da 2.5. La divisione “intera” nella versione 3 si scrive // (che in C++ è il commento).
    Altrimenti sarebbe troppo facile 😉 o anche i linguaggi di programmazione evolvono?

    • glipari  Il 3 aprile 2011 alle 00:09

      Uh, hai ragione! Io ho installato il 2.6.6, ecco perché! Consigli di upgradare al 3.0?
      Mi piace questa cosa del simbolo separato per la divisione fra interi: una delle idiosincrazie della sintassi C che non mi è mai andata giù. Vediamo se domani riesco a scrivere un pezzo sulle STL.

      • juhan  Il 3 aprile 2011 alle 07:21

        Per adesso Ubuntu continua a usare il 2.x. Sulle cose che cambiano con la 3.x dovrei fare un post. Ma a me che sono vecchio questa transizione ricorda il Fortran, dal IV al 77 con il DO (ciclo for) cambiato e gli INTEGER che passano da 2 a 4 bit, obbligandoti a controllare tutti i COMMON. La cosa andò avanti per parecchi anni.

Trackback

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo di WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.

%d blogger hanno fatto clic su Mi Piace per questo: