Dr. Carlo Pescio
Template e nomi dipendenti

Pubblicato su C++ Informer No. 5, Giugno/Luglio 1998
Avrei voluto dedicare questa puntata di ANSI/ISO C++ all'analisi dello smart pointer auto_ptr, che a fine '97 e' stato completamente stravolto rispetto al draft pubblicamente disponibile. Si tratta pero' di un argomento che richiede molto spazio, e che pertanto (come da mini-editoriale) ho deciso di rimandare ad un prossimo numero di C++ Informer.
L'argomento che vedremo in questa breve puntata e' pero' molto importante, perche' riguarda una incompatibilita' all'indietro: ovvero, una delle chiarificazioni dello standard ha reso non piu' valido codice che precedentemente veniva considerato tale. Quello che e' peggio, gran parte dei compilatori in commercio (incluso il KAI, il Visual C++ 5, l'egcs, ecc) non sono ancora allineati su questo particolare dello standard, e lasciano passare codice errato senza ne' errori ne' warning. In effetti, mi sono accorto del problema solo dopo aver ricevuto un messaggio di errore da un compilatore in alpha release, che protestava su un frammento di codice apparentemente lecito. Approfondendo l'argomento, e' emerso quanto state per leggere.

Giusto o sbagliato?
Consideriamo il seguente, semplice programma:
template< class T > class Base
  {
  public :
    void f1() {}
  } ;

template< class T > class Derived : public Base< T >
  {
  public :
    void f2() { f1() ; }
  } ;

int main()
  {
  Derived< int > d ;
  d.f2() ;
  return( 0 ) ;
  }

Apparentemente e' tutto corretto, e chiamando d.f2() chiamiamo Derived<int>::f2(), che a sua volta chiama Base<int>::f1(). Giusto? Purtroppo no, ed il codice in questione dovrebbe dare un errore di compilazione se il compilatore fosse realmente ANSI/ISO compliant.

Nomi dipendenti e non dipendenti
Secondo lo standard, alcuni nomi che compaiono all'interno di un template vengono detti "dipendenti", perche' dipendono dai parametri del template. Ad esempio, i nomi dei parametri sono (ovviamente!) nomi dipendenti. Oppure, se T e' il nome di un parametro, espressioni di tipo come B<T> sono a loro volta dipendenti, e cosi' via (per maggiori dettagli, consultate lo standard al paragrafo 14.6.2).
I nomi dipendenti vengono presi in considerazione solo al momento dell'istanziazione del template, non al momento della dichiarazione. La ragione e' tutto sommato semplice: se abbiamo un template del genere:

template< class T > class A
  {
  B< T > y ;
  } ;
B< T > e' un nome dipendente e non possiamo dargli un significato sino a quando il template A non verra' istanziato: ad esempio, se istanziamo A< int >, otteniamo B< int > e potremmo avere una specializzazione esplicita per B< int >, che richiede un codice del tutto diverso dal template "generale" B< T >.
I nomi non dipendenti, tuttavia, vengono considerati al momento della dichiarazione del template, non al momento dell'istanziazione. Ad esempio, se abbiamo un template come il seguente:

template< class T > class A
  {
  void f() { g() ; }
  } ;
il nome "g" e' un nome non dipendente dal parametro, quindi viene considerato al momento della dichiarazione: di conseguenza, se nello scope di dichiarazione di A non esiste una funzione g, avremo un messaggio di errore.
Arriviamo ora al punto cruciale per il nostro esempio: all'interno di Derived, f1 e' un nome non dipendente. I nomi non dipendenti vengono cercati al momento della dichiarazione, ma lo scope delle classi base dipendenti (come Base) non viene preso in considerazione sino al momento dell'istanziazione (punto 14.6.2.4 dello standard). Anche qui, la giustificazione e' relativamente semplice a posteriori: Base< int > potrebbe essere completamente diversa, e potrebbe anche non avere una member function f1().
Di conseguenza, il codice dato e' errato, ed avrebbe dovuto produrre un messaggio di errore del tipo "nome f1 sconosciuto" al momento del parsing della dichiarazione di Derived.
La situazione in realta' e' ancora piu' aggrovigliata. Se ci fosse stata una funzione globale f1(), il codice di cui sopra sarebbe stato legale, ma Derived<int>::f2() avrebbe chiamato la f1() globale, non Base<int>::f1().

Evitare il problema
E' evidente che non sara' poco il codice esistente che diventera' improvvisamente illegale. Se il risultato sara' un messaggio di errore in compilazione, poco male: vedremo tra poco come rimediare. Il vero problema si avra' nei casi (speriamo rari!) in cui il codice verra' compilato correttamente ma chiamera' una funzione diversa da prima. Con buona pace di chi ama tutto il bagaglio di dettagli impliciti che i template del C++ si portano dietro, e che stanno iniziando a mostrare il loro lato oscuro.
Vediamo comunque come risolvere il problema; in realta', vorremmo trovare una strategia per mettere a posto il vecchio codice e per iniziare a scrivere codice corretto d'ora in poi. Innanzitutto chiariamo bene le situazioni in cui si presenta il problema: dobbiamo avere un template, con almeno una classe base che sia a sua volta un template dipendente da uno dei parametri. In questa situazione, una chiamata ad una funzione della classe base produrra' un errore o un effetto indesiderato.
Una possibile soluzione e' l'uso dell'operatore di risoluzione di scope:
template< class T > class Derived : public Base< T >
  {
  public :
    void f2() { Base<T>::f1() ; }
  } ;

Questo trasforma f1 da nome non dipendente in nome dipendente, che come tale deve essere verificato in fase di istanziazione e non di dichiarazione. Tuttavia la tecnica non funziona se f1 e' una funzione virtuale: se abbiamo una classe template DoubleDerived, derivata da Derived e che ridefinisce f1(), il codice originale chiama (con i compilatori attuali) DoubleDerived<T>::f1(), mentre quello modificato come sopra chiama Base<T>::f1().
La tecnica piu' sicura e' a mio avviso l'uso esplicito di "this":
template< class T > class Derived : public Base< T >
  {
  public :
    void f2() { this->f1() ; }
  } ;
Siccome this e' dipendente dal parametro, l'espressione this->f1() diventa una espressione dipendente ed il risultato diventa corretto.

Conclusioni
Esempi del genere, onestamente, danno molto da pensare. Chi segue i miei articoli su Computer Programming conosce bene la mia opinione sui template in C++: estremamente potenti, ma molto fragili. Il codice basato su template ha molti dettagli inespressi dietro l'apparente semplicita' di un semplice <class T> in piu' nella dichiarazione di una classe. La corretta gestione di questi dettagli rende il modello del compilatore piu' complesso, e questo puo' giocare brutti scherzi al programmatore. I guru (o pseudo-tali) inneggiano alla flessibilita', ma stranamente pochi fanno notare i problemi. Dico "stranamente" proprio perche' l'auto-critica del linguaggio e' sempre stato un elemento caratterizzante nella cultura di fondo del C e del C++. Per qualche ragione, con i template questa tradizione di chiarezza sui potenziali problemi sembra essersi interrotta.
D'altra parte, in questo caso la soluzione diventa tutto sommato meccanica: usare in modo sistematico this-> per accedere a dati e funzioni membro all'interno dei template. A differenza di un uso selettivo, l'uso sistematico evita possibili problemi in fase di manutenzione. Il codice diventa (a mio avviso) un po' piu' macchinoso da leggere, ma credo che in casi come questo si tratti del minore dei mali.

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.