Dr. Carlo Pescio
Deprecation Java-like

Pubblicato su C++ Informer No. 12, Ottobre 2000

Conoscere piu' di un linguaggio di programmazione ha molti risvolti utili. Uno di questi e' la possibilita' di "importare" caratteristiche ed idiomi di programmazione tipici di un linguaggio all'interno di un altro, magari come elementi di libreria realizzati nel linguaggio stesso.
Ad esempio, sul numero 2 di C++ Informer, sempre nella rubrica "No Limits", ho presentato alcune classi riusabili per implementare in C++ una funzionalita' built-in di Java, ovvero la possibilita' di creare una istanza di una classe il cui nome sia noto sotto forma di stringa.
Alcuni mesi fa, un lettore di C++ Informer mi ha suggerito di "importare" in C++ un'altra utile funzionalita' di Java: il tag "deprecated" per i metodi (il lettore ha peraltro vinto una copia del mio C++ Manuale di Stile grazie a questo suggerimento).
L'idea di dichiarare un metodo (o una classe, o un data member) come "deprecated" e' interessante perche' va incontro ad una visione della programmazione come comunicazione uomo-uomo e non soltanto uomo-macchina. E' infatti inevitabile, per chi scrive codice che viene effettivamente riusato, scoprire solo in fasi piu' avanzate che alcune funzioni offerte potevano essere implementate in modo differente e migliore. Se la modifica ha solo un impatto sulla parte implementativa, e non influenza l'interfaccia (pubblica/protetta) della classe, questo non costituisce un problema. In alcune situazioni, tuttavia, ci accorgiamo che per migliorare realmente l'implementazione e' necessario cambiare anche l'interfaccia della classe, o talvolta anche spostare il metodo altrove, magari in una nuova classe nata per l'occasione.
Una opportunita' sul breve/medio termine e' mantenere (se possibile) la vecchia interfaccia insieme alla nuova, e "deprecare" la vecchia interfaccia. Si auspica quindi che nel tempo i nuovi utilizzatori facciano riferimento alla nuova interfaccia, e che anche il codice preesistente, durante la manutenzione, converta le chiamate ai vecchi metodi in chiamate "equivalenti" ai nuovi. Esiste solo un piccolo problema di fondo: senza un avvertimento da parte del compilatore, il programmatore che non si legga attentamente la documentazione o gli header file non avra' modo di sapere che alcuni metodi sono stati deprecati.
Ecco quindi nascere l'idea (non indispensabile da un punto di vista strettamente formale, ma come dicevo interessante da un punto di vista dell'ingegneria del software) di apporre nel punto di dichiarazione dell'elemento deprecato il tag "deprecated". Sara' il compilatore a far osservare al programmatore (ad esempio tramite un warning) che sta utilizzando una funzionalita' deprecata.

Per implementare qualcosa di analogo in C++ standard (ovviamente senza modificare il compilatore!) dobbiamo muoverci in un terreno minato :-), perche' poco sopra ho nominato i warning, e questi sono "esclusi" dallo standard ANSI/ISO C++. Si preannuncia anche piuttosto complicato, perche' vogliamo che la nostra implementazione di "deprecated" soddisfi almeno le seguenti caratteristiche:

1) Il codice preesistente (corretto) deve rimanere tale; e' accettabile doverlo ricompilare (quindi: compatibilita' sintattica dei chiamanti, non necessariamente compatibilita' binaria).

2) Deve essere emesso un warning solo se si fa uso della funzionalita' deprecata (es. chiamando un metodo deprecato). Se si include semplicemente la sua dichiarazione (es. in un header) ma non se ne fa uso, non vorremmo venisse emesso alcun messaggio.

3) Il messaggio dovrebbe essere ragionevolmente chiaro ed esplicativo.

4) Come sempre, vorremmo una implementazione quanto piu' possibile standard (pur nei limiti dovuti alla non standardizzazione dei warning).

5) Vorremmo che fosse semplice da usare, [quasi] come una funzionalita' built-in del linguaggio.

Per contro, potremmo limitarci ad introdurre il nostro "deprecated" solo per le member function. In fondo, di solito per deprecare una classe basta deprecare i suoi costruttori. I data member non dovrebbero comunque essere accessibili dall'esterno, e quindi dovrebbe essere meno sentita l'esigenza di deprecarli. Deprecare una interfaccia (in C++, una classe con solo funzioni virtuali pure, e quindi tipicamente priva di costruttore) e' invece piu' faticoso, in quanto ci costringe a deprecare (in teoria) tutte le sue funzioni (magari per un numero futuro cerchero' di estendere direttamente a livello di classi e di interfacce quanto segue). D'altra parte, se esiste un distruttore virtuale pubblico, potrebbe essere sufficiente deprecarlo per ottenere un risultato che si avvicina alla deprecazione dell'interfaccia.

Ingredienti
Come sempre, gli articoli di "No Limits" non vogliono soltanto affrontare un problema "difficile" e stupire con effetti speciali :-), ma presentare alcuni aspetti del linguaggio da una prospettiva differente. In questo modo, chi legge ha la possibilita' di applicare tecniche analoghe anche a problemi diversi da quelli qui presentati. Nel caso particolare, vediamo come si puo' arrivare ad una ipotesi di soluzione per il problema, affrontando uno ad uno i diversi punti.

E' evidente che per far emettere un warning dovremmo in qualche modo "cambiare" la funzione. Non possiamo pero' intervenire sulla sua implementazione: vogliamo che il warning venga emesso nel momento della chiamata, che e' presumibilmente in una unita' di compilazione diversa dall'implementazione della funzione.
Dobbiamo quindi cambiare il prototipo della funzione, in modo tale che il codice vecchio continui a funzionare, ma che emetta un warning nei punti di chiamata. In effetti non ci sono molte strade a disposizione: possiamo pero' aggiungere un parametro alla funzione, con un valore di default. Questo garantisce la compatibilita' sintattica (non binaria) con il codice esistente. Quindi, se il nostro codice preesistente e' del tipo:

class A
  {
  public :
    void f( int x, int y ) ;
    // ...
  } ;

e vogliamo deprecare f, potremmo iniziare a trasformarlo in :
class A
  {
  public :
    void f( int x, int y, XXX deprecated = YYY ) ;
    // ...
  } ;

Dove XXX ed YYY sono ancora da determinare, ma dovranno essere gli elementi chiave per far emettere il warning. E qui arriva un punto un po' ostico da superare: dobbiamo far si' che il warning venga emesso solo nel punto di chiamata, e solo se vi sono delle chiamate.

I lettori di C++ Informer dovrebbero ormai sapere che non si puo' compiere un rito di magia nera in C++ senza utilizzare i template :-). Anche in questo caso i template possono venirci in aiuto, se pensiamo ad una loro caratteristica particolare, sancita dallo standard: se dichiariamo una template class, le sue member function vengono singolarmente istanziate solo se vengono utilizzate (si veda lo standard al paragrafo 14.7.1p9). Altrimenti, il compilatore non deve neppure tentare di istanziarle.
Possiamo quindi iniziare ad ipotizzare che XXX o YYY coinvolgano una template class che, quando viene istanziato uno dei suoi metodi, produca un warning. Per proseguire su questa strada e' indispensabile verificare meglio cosa garantisce lo standard riguardo l'interazione tra argomenti di default e template class. Questo chiarimento e' presente sempre al punto 9 del paragrafo 14.7.1 ("Template instantiation and specialization"), che recita quanto segue: "the use of a template specialization in a default argument shall not cause the template to be implicitly instantiated except that a class template may be instantiated where its complete type is needed to determine the correctness of the default argument". Quindi, se utilizziamo una specializzazione di un template in un argomento di default, il template non verra' istanziato (salvo casi particolari da cui dobbiamo tenerci lontani). Il punto 9 prosegue sui binari desiderati aggiungendo: "The use of a default argument in a function call causes specializations in the default argument to be implicitly instantiated", ovvero, il template verra' instanziato nel punto di chiamata alla funzione f. Ci stiamo decisamente avvicinando ad una soluzione.

Dobbiamo ancora trovare uno stratagemma per far emettere un warning quando una member function di una template class viene istanziata. Per fare qualche esperimento, e' sufficiente pensare a come fare emettere un warning quando una comune funzione viene compilata. Per ragioni che vedremo tra poco, ipotizziamo una funzione senza parametri.
Come gia' accennato, lo standard non si occupa dei warning, e questo lascia alle singole implementazioni una notevole flessibilita'. Dal canto nostro, dobbiamo anche stare attenti a far generare un warning "innocuo", poiche' il codice della funzione verra' poi effettivamente eseguito come effetto collaterale della chiamata ad un metodo deprecato. Se il warning sottintende un reale problema run-time, il metodo diventa un po' troppo deprecato :-)).
Un warning innocuo a run-time, ed emesso da quasi tutti i compilatori e' quello di "parametro non utilizzato", che tuttavia non possiamo sfruttare poiche' la nostra funzione non avra' parametri. Un altro warning innocuo che viene spesso emesso riguarda le variabili locali dichiarate ma non utilizzate, ma non tutti i compilatori emettono un warning in questo caso. Una soluzione piuttosto generale e' dichiarare una variabile locale di tipo integral, non inizializzarla, e leggerne il valore, come nel codice seguente:

void f()
  {
  int deprecated ; 
  int deprecated2 = deprecated ; 
  }
Sia il gcc 2.95 (con il flag -Wall per avere tutti i warning), sia il Visual C++ 6, sia il Borland 5.5, sia altri compilatori ancora, emettono un warning in questo caso.

Abbiamo ormai tutti gli ingredienti necessari per passare alla reale implementazione del nostro "deprecated". Il punto piu' ostico si rivelera', come vedremo (e forse un po' inaspettatamente per chi non ha giocato molto con la magia compile-time), il numero (3) del mio elenco: ottenere un messaggio ragionevolmente chiaro ed esplicativo. Vedremo che la diagnostica di diversi compilatori lascia decisamente a desiderare sotto questo singolo aspetto. Non a caso, ho disseminato di variabili chiamate "deprecated" gli esempietti visti sopra: la recondita speranza e' che il compilatore generi un warning piu' "leggibile" anche grazie a questi semplici espedienti.

Soluzione
Partiamo da una situazione preesistente, ovvero una classe come la seguente:
// a.h
class A
  {
  public :
    A() ;
    void f() ;
    void g() ;
  private :
    int x ;
  } ;


// a.cpp
#include <iostream>
#include "a.h"

A :: A()
  {
  x = 0 ;
  }

void A :: f()
  {
  ++x ;
  std::cout << x << " " ;
  }

void A :: g()
  {
  ++x ;
  std::cout << x << " " ;
  }


// main.cpp
#include "a.h"

int main()
  {
  A a ;
  a.f() ;       //(W)
  a.g() ;
  return( 0 ) ;
  }
Per adesso, e' solo un modo complicato per far stampare "1 2" sullo standard output :-). Il nostro obiettivo e' creare un supporto di libreria che renda facile deprecare, ad esempio, A::f, lasciando il programma su riportato compilabile e inalterato nel suo comportamento osservabile, ma con l'emissione di un warning nel punto segnato con //(W). Sostituendo la riga in questione con la chiamata a.g(), il programma dovra' rimanere inalterato nel comportamento osservabile, e dovra' anche sparire il warning.

Iniziamo creando una template class contenente una member function che, se istanziata, produca un warning. Ho scelto il distruttore come member function, per ragioni su cui tornero' fra breve.

// deprecated.h
template< int N > struct Deprecated
  {
  Deprecated( int ) {}
  ~Deprecated()
    {
    int deprecated ; 
    int deprecated2 = deprecated ; 
    }
  } ;
Modifichiamo poi la dichiarazione (e l'implementazione) della classe A come segue:

// a.h
#include "deprecated.h"

class A
  {
  public :
    A() ;
    void f( const Deprecated< 0 >& d = 0 ) ; // deprecated method
    void g() ;
  private :
    int x ;
  } ;


//a.cpp
#include <iostream>
#include "a.h"

A :: A()
  {
  x = 0 ;
  }

void A :: f( const Deprecated< 0 >& )
  {
  ++x ;
  std::cout << x << " " ;
  }

void A :: g()
  {
  ++x ;
  std::cout << x << " " ;
  }
Notiamo come la dichiarazione di A::f sia stata modificata, aggiungendo il parametro (con valore di default) "const Deprecated< 0 >& d = 0". Corrispondentemente, la sua implementazione prendera' un parametro in piu', che ho lasciato senza nome in modo da evitare un diverso tipo di warning (in questo caso indesiderato). Spieghero' tra breve perche' ho adottato un parametro di tipo reference, chiarendo cosi' anche perche' ho scelto il distruttore come member function con emissione di warning.
Per ora, vorrei far osservare che non vi e' alcuna necessita' di modificare gli utilizzatori della classe A (in questo caso main.cpp); come da requisito (1), il codice modificato deve rimanere compatibile, a livello sorgente, con il codice esistente.
Dal punto di vista delle modifiche al sorgente per la deprecazione di A::f abbiamo finito: compilando il codice modificato come sopra, otterremo un warning dovuto alla chiamata a.f() all'interno di main(). Se la sostituiamo con a.g(), sparisce il warning al momento della compilazione.

Ultime spiegazioni
Credo sia utile ricapitolare un po' il funzionamento di quanto presentato sopra; approfittero' cosi' dell'occasione per chiarire i due punti lasciati in sospeso, riguardo il passaggio per reference ed la scelta di generare i warning nel distruttore.
La tecnica di base dovrebbe ormai essere ovvia: aggiungere un parametro con valore di default alla funzione deprecata, in modo da preservare la compatibilita' a livello sorgente dei chiamanti, e sfruttare il parametro di default per far generare un errore nel momento della chiamata.
In particolare, per ottenere un errore solo se la funzione deprecata viene effettivamente chiamata, usiamo come tipo del parametro di default una specializzazione di un template. Lo standard ci garantisce che il template non verra' implicitamente specializzato nel punto di dichiarazione, ma solo nel punto di chiamata. Dobbiamo far si' che specializzando il template venga generato un warning, e questo significa dotare una funzione del template (che venga specializzata) di codice atto ad emettere un warning.
In teoria, per fare questo sarebbe sufficiente usare il passaggio per valore (senza necessita' del reference) per il parametro, e far si' che il costruttore (o il costruttore di copia) della classe Deprecated generi il warning quando istanziato.
Tuttavia, come avete letto nella citazione riportata in apertura, lo standard lascia al compilatore la possibilita' di specializzare implicitamente il template se "necessario per determinare la correttezza del parametro di default". Purtroppo anche leggendo attentamente lo standard non e' proprio chiaro cosa questo significhi, ed ho quindi cercato di prendere alcune contromisure. In particolare, per evitare problemi nel caso qualche compilatore troppo zelante tenti una istanziazione del template a causa del passaggio per valore (che richiede la verifica sul costruttore di copia) ho optato per il passaggio per riferimento. Inoltre, sempre per evitare warning nel caso in cui il compilatore decida di istanziare comunque il costruttore utilizzato per creare il valore di default (per "determinarne la correttezza"), ho lasciato tale costruttore vuoto. A questo punto, la soluzione piu' semplice e sicura e' generare il warning nel distruttore, che verra' comunque istanziato (nel punto di chiamata) per distruggere il temporaneo passato come valore di default. Purtroppo, con qualche compilatore anche queste contromisure non sono sufficienti (come vedremo nel paragrafo successivo).

Osserviamo che la tecnica usata ha una controindicazione: creare, passare per reference e poi distruggere un oggetto di classe Deprecated ha comunque un prezzo, per quanto piccolo, in termini di performance. Se la funzione deprecata e' piccola, come un metodo di accesso ad un data member, il costo puo' risultare sensibile.
Visto nella giusta prospettiva :-), tuttavia, questo difetto diventa quasi un pregio: stiamo deprecando una funzione perche' ne esiste una versione alternativa che si suppone "migliore". Il fatto che la versione deprecata diventi un po' piu' lenta puo' essere un ulteriore incentivo per rimuovere le relative chiamate anche dal codice esistente.

Il maggiore difetto dell'intera tecnica, come stiamo per vedere, riguarda comunque le capacita' dei diversi compilatori di emettere una diagnostica significativa.

Risultati sui diversi compilatori
E' ora il momento di passare dalla teoria alla pratica e vedere come si comporta il codice dato sopra con dei compilatori "veri". Purtroppo il risultato lascia un po' a desiderare, a causa delle carenze diagnostiche di diversi compilatori commerciali. In quanto segue, vedremo cosa accade compilando il codice visto sopra con il Visual C++ 6, il Borland C++ 5.5 ed lo GNU C++ 2.95. Accennero' poi ad altri compilatori.

Iniziamo con il Visual C++ 6, con level warning standard (livello 3). Otteniamo un messaggio di errore molto scarno:

main.cpp 
c++informer\issues\#12\src\deprecated.h(7) : 
warning C4700: local variable 'deprecated' used without having been initialized
Se il warning level e' posto al massimo (livello 4), dopo una quantita' industriale di warning generati dall'inclusione dell'header standard <iostream> (una vergogna per chi ha implementato la libreria standard: si trattava di warning tranquillamente evitabili) otteniamo un messaggio piu' dettagliato ma non necessariamente piu' utile:
main.cpp
c++informer\issues\#12\src\deprecated.h(7) : 
warning C4189: 'deprecated2' : local variable is initialized but not referenced
c++informer\issues\#12\src\deprecated.h(5) : 
while compiling class-template member function '__thiscall Deprecated<0>::~Deprecated<0>(void)'
c++informer\issues\#12\src\deprecated.h(7) : 
warning C4700: local variable 'deprecated' used without having been initialized
Purtroppo, come potete notare, in entrambi i casi il messaggio fa riferimento solo alla linea del template che ha causato l'errore. Aver utilizzato tutte variazioni di "deprecated" per i nomi di classi e variabili aiuta a capire che si tratta di un warning dovuto ad una funzione deprecata. Tuttavia abbiamo ben pochi elementi per capire dove avvenga la chiamata. Paradossalmente, l'informazione piu' utile rischia di venire trascurata: si tratta dell'indicazione "main.cpp", ovvero del file che si sta compilando. In definitiva, pero', il messaggio e' del tutto insufficiente a far identificare la riga problematica in progetti reali, dove una compilation unit e' ben piu' lunga del nostro "main.cpp".
A dire il vero, questo e' solo un esempio delle carenze diagnostiche del compilatore in questione. Al di la' del nostro esercizio "estremo", in situazioni "reali" avere scarse informazioni riguardo i punti di chiamata e di istanziazione puo' rendere il debugging decisamente piu' difficile.

Vediamo ora come si comporta il Borland C++ 5.5, con tutti i warning abilitati. Utilizzando l'ambiente integrato, otteniamo il seguente messaggio:

[C++ Warning] deprecated.h(7): 
W8013 Possible use of 'deprecated' before definition
[C++ Warning] deprecated.h(9): 
W8004 'deprecated2' is assigned a value that is never used
In effetti e' un ulteriore peggioramento rispetto al Visual C++: sparisce anche l'informazione essenziale, data dal nome del file sotto compilazione. Un aspetto decisamente negativo per un tool che punta molto sullo sviluppo rapido, perche' una diagnostica che non sa dare indicazioni utili per rintracciare i problemi (in questo caso "benigni", ma non sempre saremo cosi' fortunati) non aiuta certo a completare piu' rapidamente un progetto. Paradossalmente, se invochiamo il compilatore da linea di comando otteniamo un messaggio piu' preciso, che indica anche il file che stiamo compilando (main.cpp). Segno che il back-end e' sotto questo punto di vista meglio del front-end, che forse in questo caso tentando di semplificare la vita al programmatore finisce per complicargliela.

Passiamo quindi al gcc 2.95, tutti i warning abilitati. Otteniamo il seguente messaggio:

deprecated.h: 
  In method 'Deprecated<0>::~Deprecated()':
main.cc:6:   
  instantiated from here
deprecated.h:7: 
  warning: unused variable 'int deprecated2'
Questo e' sicuramente il messaggio di errore piu' indicativo. La presenza dei vari "deprecated" nel warning ci fa capire che stiamo chiamando una funzione deprecata. La presenza del file e del numero di riga che hanno causato l'istanziazione di ~Deprecated() (main.cc, linea 6) ci portano facilmente ad identificare la chiamata a.f() come colpevole.
E' sempre interessante osservare come prodotti distribuiti gratuitamente si pongano ad un livello superiore rispetto a prodotti commerciali molto diffusi. A mio avviso, questo e' in larga misura dovuto all'enfasi del "team gcc" sulle feature del compilatore, piuttosto che ad elementi importanti ma separati come l'ambiente integrato, gli aspetti RAD e visuali, il supporto a funzionalita' platform-specific, eccetera.

Notiamo anche che il gcc segnala solo il problema relativo alla variabile "deprecated2", mentre il Visual C++ a livello 3 solo il problema relativo alla variabile "deprecated". Questa e' la ragione che mi ha portato a mettere entrambe le righe "problematiche" nel distruttore, in modo da avere un messaggio da entrambi i compilatori. E' possibile che altri compilatori richiedano un ulteriore riaggiustamento del sorgente per generare un warning.

Infine, va detto che alcuni compilatori (es. Comeau C++) producono sempre un warning, anche se A::f() non viene chiamata. In pratica, il distruttore viene istanziato comunque, in base alla semplice dichiarazione di un parametro con valore di default. Questa e' una violazione dello standard, che purtroppo impedisce alla soluzione presentata di funzionare correttamente.

Considerazioni finali
Ancora una volta in "No Limits" abbiamo visto un esempio "estremo" di utilizzo del C++ come linguaggio "a due livelli" (vi rimando alla rubrica ANSI/ISO di questo stesso numero per ulteriori considerazioni in proposito). Utilizzando i building-block del linguaggio siamo [quasi :-)] riusciti a ricreare, all'interno del linguaggio stesso, una feature che in altri ambienti e' realizzata dal compilatore.

Per l'utilizzatore finale, la differenza e' quasi trascurabile: potremmo infatti aggiungere una macro DEPRECATED e far si' che la sintassi per la nostra funzione deprecata si semplifichi ulteriormente, diventando:

void f( DEPRECATED ) ;
Dal punto di vista di chi realizza librerie, componenti, framework et similia, invece, la differenza tra un linguaggio "ad un livello" come Java e tanti altri, ed un linguaggio "a due livelli" come il C++ (e pochi altri, tra cui in larga misura Smalltalk e la famiglia LISP) e' decisamente notevole. In C++ si puo' (anzi, si deve!) lavorare ad un livello di astrazione elevato in gran parte del codice, ed al contempo scendere secondo necessita' a basso livello e costruire nuovi elementi per il livello superiore, sempre senza abbandonare il linguaggio. L'errore piu' frequente di chi utilizza il C++ e' invece quello di usarlo come un linguaggio "ad un livello", programmando sempre in termini di building block elementari e producendo quindi grandi quantita' di codice intricato. Ovviamente, trovare il giusto punto di equilibrio non e' semplice, e richiede esperienza ed intelligenza; personalmente, tuttavia, credo che l'approccio (non necessariamente il linguaggio) adottato dal C++ sia il piu' interessante nel panorama della programmazione contemporanea.

Biografia
Carlo Pescio (pescio@eptacom.net) svolge attività di consulenza, progettazione e formazione in ambito internazionale. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. È autore di numerosi articoli su temi di ingegneria del software, programmazione C++ e tecnologia ad oggetti, apparsi sui principali periodici statunitensi, nonché dei testi "C++ Manuale di Stile" ed "UML Manuale di Stile". Laureato in Scienze dell'Informazione, è membro dell'ACM, dell'IEEE e dell'IEEE Technical Council on Software Engineering.