Anni fa, in occasione dell'uscita della versione 5 del linguaggio Java (release Tiger) tenni un seminario di dipartimento presso l'Universita' degli Studi di Modena e Reggio Emilia nel quale presentavo tutte le novita' introdotte nel linguaggio (qualche copia del seminario si trova ancora in giro, anche se non garantisco l'autenticita' - chi vuole averne una copia autentica mi contatti).
Una delle novita' di Java 5 erano i "Java Generics". In occasione della presentazione di generics, una eminenza grigia, professorone con anni di esperienza nella OOP e nella programmazione C++, con tono sprezzante e quasi schifato disse:
All'epoca ero troppo educato per rispondere male! Con questo articolo pero' voglio mettere in chiaro le differenze fra i templates del C++ e i generics di Java, sottolineando come il pensiero sopra espresso dal professore universitario sia ben lontano dalla realta'.
Si consideri Java Generics, di cosa si tratta? Generics e' un modo per evitare errori grossolani di conversione di tipi facendo fare del lavoro sporco al compilatore. Il compilatore Java e' sempre stato fortemente tipizzato, ma solo per quello che riguarda i tipi primitivi. Nel caso degli oggetti, grazie al principio di sostituzione e alla base comune, Object, ogni oggetto potrebbe essere convertito in uno di un altro tipo passando per un Object. Questo genera una serie di errori di coerenza come il seguente:
Persona madre = new Persona();
Persona padre = new Persona();
Animale cane = new Animale();
List famigliari = new LinkedList();
famigliari.add( madre );
famigliari.add( padre );
famigliari.add( cane ); // disastro!
A meno che non si abbia una forte considerazione del proprio cane, esso non andrebbe inserito nella lista dei famigliari (il fatto che il proprio cane debba essere inserito o meno nella lista dei famigliari è un argomento che esula da generics!). Il problema nasce a run-time, quando si cerchera' di manipolare gli oggetti eseguendo dei cast che non saranno verificati.
Generics permette di intercettare tali errori permettendo ad una classe di definire dei "segnaposto" generici il cui tipo verra' specificato al momento della creazione dell'istanza. In altre parole il codice di cui sopra diviene:
Persona madre = new Persona();
Persona padre = new Persona();
Animale cane = new Animale();
List famigliari = new LinkedList();
famigliari.add( madre );
famigliari.add( padre );
famigliari.add( cane ); // errore di compilazione
In questo caso il compilatore sa che la lista deve accettare solo istanze valide di Persona, e quindi rifiuta di compilare un programma con un simile errore di logica.
Si noti che il controllo di coerenza avviene solo a compile-time: a run-time l'informazione sui tipi viene totalmente rimossa (type erasure) e tutti i tipi generici sono sostituiti con Object. Questo e' dovuto al fatto che un programma compilato con Generics deve poter eseguire anche su una virtual machine che di generics non sa nulla.
I template C++ sono differenti. Anzitutto i template consentono due funzionalita': generics e meta-programming. La prima funzionalita' e' analoga a quella di Java: si definisce un marcaposto per un tipo e tale tipo verra' specificato al momento della istanziazione (nel codice). Il programma di cui sopra diventa:
class Persona{};
class Animale{};
template< class L >
class List{
private:
L elements[3];
public:
inline void add( L newElement ){
// add code
}
};
// quando occorre usare la lista
List famigliari < Persona >;
famigliari.add( padre );
famigliari.add( madre );
famigliari.add( cane ); // errore di compilazione
Il sistema riporta un errore di compilazione simile al seguente:
error: no matching function for call to ‘List::add(Animale&)’
note: candidates are: void List::add(L) [with L = Persona]
Potrebbe sembrare che almeno nella implementazione di Generics Java e C++ siano identici, in realta' C++ permette la definizione di classi compilate in modo differente per ogni tipo specificato. Quindi mentre in Java esiste una sola classe List che accetta degli Object controllati dal compilatore, in C++ possono esistere piu' versioni della lista compilata ciascuna con ogni tipo richiesto.
Ma la differenza fra i templates C++ e Java Generics diventa ancora piu' evidente quando si parla di meta-programming. In sostanza i templates C++ possono essere applicati non solo a delle classi, ma anche a dei metodi:
template
T findMax (T a, T b) {
T result;
result = (a>b)? a : b;
return (result);
}
ovviamente per avere senso i template applicati ai metodi dovranno spesso sfruttare anche l'overloading degli operatori.
Infine il C++ utilizza i template per "capire", a tempo di compilazione, come parametrizzare il codice che genera. E' quindi possibile forzare non solo il tipo di un parametro, ma anche il valore, in modo da costringere il compilatore a compilare il codice con un parametro inserito in modo hard-coded. Si consideri il seguente esempio:
template< int printCount, int spaces >
class Printer{
public:
static void print(){
string printString = "Hello World!";
for( int i = 0; i < printCount; i++ ){
// stampo gli spazi
for( int j = 0; j < spaces; j++ )
cout << " ";
// stampo la linea e l'andata a capo
cout << i << " " << printString << endl;
}
}
void croack(){
string printString = "Hello World!";
for( int i = 0; i < printCount; i++ ){
// stampo gli spazi
for( int j = 0; j < spaces; j++ )
cout << " ";
// stampo la linea e l'andata a capo
cout << i << " " << printString << endl;
}
}
};
int main( int argc, char** argv ){
cout << "Programma in esecuzione" << endl;
Printer< 5, 5 >::print();
Printer< 2 , 8> myPrinter;
myPrinter.croack();
cout << "Programma terminato" << endl;
}
La classe Printer definisce due metodi identici: print (statico) e croack (di istanza). Tali metodi traggono vantaggio del valore di due parametri specificati come template (spaces e printCount) per stampare un certo numero di volte e con un certo numero di spazi di indentazione la famosa stringa "Hello World". La cosa interessante, e che Java non permette, e' che il valore dei parametri viene specificato direttamente al momento della chiamata della funzione statica o della creazione dell'istanza. In Java la classe Printer avrebbe dovuto avere una implementazione simile alla seguente:
public class Printer{
PRINTCOUNT printCount;
SPACES spaces;
public Printer( PRINTCOUNT count, SPACES spcs ){
printCount = count;
spaces = spcs;
}
// metodi analoghi alla classe C++
}
e la si sarebbe dovuta istanziare cosi':
Printer printer = new Printer(5, 10);
senza la possibilita' di usare il metodo statico direttamente.
Riassumendo quindi Generics e' solo una branchia del template programming, che si estende ben oltre la semplice "specifica variabile dei tipi" e i "controlli di compilazione". L'idea dietro al templating e' si quella di creare classi generiche, ma anche algoritmi generici (come la funzione findMax di cui sopra) e dare la possibilita' al compilatore di capire cosa succedera' a run-time, generando codice specifico per il programma.
In considerazione di questi semplici esempi didattici, e considerata l'affermazione del professore riportata all'inizio, viene da chiedersi se tale professore sia maggiormente incompetente in Java, in C++ o in generale nella OOP. E la cosa che fa ancora piu' amarezza, e' che tale professore continuera' a formare studenti e programmatori che dovranno apprendere da soli cosa realmente siano i templates.
Una delle novita' di Java 5 erano i "Java Generics". In occasione della presentazione di generics, una eminenza grigia, professorone con anni di esperienza nella OOP e nella programmazione C++, con tono sprezzante e quasi schifato disse:
hanno copiato i templates C++ e li hanno applicati a Java, nulla di nuovo!
All'epoca ero troppo educato per rispondere male! Con questo articolo pero' voglio mettere in chiaro le differenze fra i templates del C++ e i generics di Java, sottolineando come il pensiero sopra espresso dal professore universitario sia ben lontano dalla realta'.
Si consideri Java Generics, di cosa si tratta? Generics e' un modo per evitare errori grossolani di conversione di tipi facendo fare del lavoro sporco al compilatore. Il compilatore Java e' sempre stato fortemente tipizzato, ma solo per quello che riguarda i tipi primitivi. Nel caso degli oggetti, grazie al principio di sostituzione e alla base comune, Object, ogni oggetto potrebbe essere convertito in uno di un altro tipo passando per un Object. Questo genera una serie di errori di coerenza come il seguente:
Persona madre = new Persona();
Persona padre = new Persona();
Animale cane = new Animale();
List famigliari = new LinkedList();
famigliari.add( madre );
famigliari.add( padre );
famigliari.add( cane ); // disastro!
A meno che non si abbia una forte considerazione del proprio cane, esso non andrebbe inserito nella lista dei famigliari (il fatto che il proprio cane debba essere inserito o meno nella lista dei famigliari è un argomento che esula da generics!). Il problema nasce a run-time, quando si cerchera' di manipolare gli oggetti eseguendo dei cast che non saranno verificati.
Generics permette di intercettare tali errori permettendo ad una classe di definire dei "segnaposto" generici il cui tipo verra' specificato al momento della creazione dell'istanza. In altre parole il codice di cui sopra diviene:
Persona madre = new Persona();
Persona padre = new Persona();
Animale cane = new Animale();
List
famigliari.add( madre );
famigliari.add( padre );
famigliari.add( cane ); // errore di compilazione
In questo caso il compilatore sa che la lista deve accettare solo istanze valide di Persona, e quindi rifiuta di compilare un programma con un simile errore di logica.
Si noti che il controllo di coerenza avviene solo a compile-time: a run-time l'informazione sui tipi viene totalmente rimossa (type erasure) e tutti i tipi generici sono sostituiti con Object. Questo e' dovuto al fatto che un programma compilato con Generics deve poter eseguire anche su una virtual machine che di generics non sa nulla.
I template C++ sono differenti. Anzitutto i template consentono due funzionalita': generics e meta-programming. La prima funzionalita' e' analoga a quella di Java: si definisce un marcaposto per un tipo e tale tipo verra' specificato al momento della istanziazione (nel codice). Il programma di cui sopra diventa:
class Persona{};
class Animale{};
template< class L >
class List{
private:
L elements[3];
public:
inline void add( L newElement ){
// add code
}
};
// quando occorre usare la lista
List
famigliari.add( padre );
famigliari.add( madre );
famigliari.add( cane ); // errore di compilazione
Il sistema riporta un errore di compilazione simile al seguente:
error: no matching function for call to ‘List
note: candidates are: void List
Potrebbe sembrare che almeno nella implementazione di Generics Java e C++ siano identici, in realta' C++ permette la definizione di classi compilate in modo differente per ogni tipo specificato. Quindi mentre in Java esiste una sola classe List che accetta degli Object controllati dal compilatore, in C++ possono esistere piu' versioni della lista compilata ciascuna con ogni tipo richiesto.
Ma la differenza fra i templates C++ e Java Generics diventa ancora piu' evidente quando si parla di meta-programming. In sostanza i templates C++ possono essere applicati non solo a delle classi, ma anche a dei metodi:
template
T findMax (T a, T b) {
T result;
result = (a>b)? a : b;
return (result);
}
ovviamente per avere senso i template applicati ai metodi dovranno spesso sfruttare anche l'overloading degli operatori.
Infine il C++ utilizza i template per "capire", a tempo di compilazione, come parametrizzare il codice che genera. E' quindi possibile forzare non solo il tipo di un parametro, ma anche il valore, in modo da costringere il compilatore a compilare il codice con un parametro inserito in modo hard-coded. Si consideri il seguente esempio:
template< int printCount, int spaces >
class Printer{
public:
static void print(){
string printString = "Hello World!";
for( int i = 0; i < printCount; i++ ){
// stampo gli spazi
for( int j = 0; j < spaces; j++ )
cout << " ";
// stampo la linea e l'andata a capo
cout << i << " " << printString << endl;
}
}
void croack(){
string printString = "Hello World!";
for( int i = 0; i < printCount; i++ ){
// stampo gli spazi
for( int j = 0; j < spaces; j++ )
cout << " ";
// stampo la linea e l'andata a capo
cout << i << " " << printString << endl;
}
}
};
int main( int argc, char** argv ){
cout << "Programma in esecuzione" << endl;
Printer< 5, 5 >::print();
Printer< 2 , 8> myPrinter;
myPrinter.croack();
cout << "Programma terminato" << endl;
}
La classe Printer definisce due metodi identici: print (statico) e croack (di istanza). Tali metodi traggono vantaggio del valore di due parametri specificati come template (spaces e printCount) per stampare un certo numero di volte e con un certo numero di spazi di indentazione la famosa stringa "Hello World". La cosa interessante, e che Java non permette, e' che il valore dei parametri viene specificato direttamente al momento della chiamata della funzione statica o della creazione dell'istanza. In Java la classe Printer avrebbe dovuto avere una implementazione simile alla seguente:
public class Printer
PRINTCOUNT printCount;
SPACES spaces;
public Printer( PRINTCOUNT count, SPACES spcs ){
printCount = count;
spaces = spcs;
}
// metodi analoghi alla classe C++
}
e la si sarebbe dovuta istanziare cosi':
Printer
senza la possibilita' di usare il metodo statico direttamente.
Riassumendo quindi Generics e' solo una branchia del template programming, che si estende ben oltre la semplice "specifica variabile dei tipi" e i "controlli di compilazione". L'idea dietro al templating e' si quella di creare classi generiche, ma anche algoritmi generici (come la funzione findMax di cui sopra) e dare la possibilita' al compilatore di capire cosa succedera' a run-time, generando codice specifico per il programma.
In considerazione di questi semplici esempi didattici, e considerata l'affermazione del professore riportata all'inizio, viene da chiedersi se tale professore sia maggiormente incompetente in Java, in C++ o in generale nella OOP. E la cosa che fa ancora piu' amarezza, e' che tale professore continuera' a formare studenti e programmatori che dovranno apprendere da soli cosa realmente siano i templates.
1 commento:
ho preso nota! ^^
Posta un commento