Dr. Carlo Pescio
Template Circoscritti

Pubblicato su C++ Informer No. 11, Aprile 2000

Poche settimane fa, scrivendo un po' di codice che usava in modo abbastanza intenso STL, mi sono trovato di fronte ad un errore particolarmente illeggibile. Nulla di nuovo, perche' i compilatori sono ancora piuttosto arretrati nelle loro funzionalita' di error reporting, soprattutto quando sono coinvolti i template. Tuttavia, l'errore in questione era particolarmente "irragionevole", perche' tirava in ballo la classe std::complex (che implementa i numeri complessi) e nulla nel mio codice faceva riferimento ai numeri complessi.
Per eccesso di fiducia :-), ho pensato ad un errore del compilatore nel riportare l'errore, ho evitato di perdermi negli header della libreria standard, ed ho cosi' capito rapidamente il reale errore (sommavo un intero con la variabile sbagliata, che non supportava la somma).
Purtroppo analoghi errori illeggibili non sono poi cosi' rari. E non sempre sono causati da un compilatore di bassa qualita': spesso sono dovuti allo stile di programmazione adottato da chi scrive librerie basate sui template, ed a voler guardare bene, spesso sono dovuti allo stile di programmazione generica adottato in C++.
Come molti di voi sapranno (consiglio a chi vuole approfondire i miei articoli [1] e [2]) in C++ la programmazione generica segue lo schema dei vincoli impliciti: si definisce una classe o una funzione generica attraverso un template, senza specificare in modo esplicito i vincoli che il tipo degli argomenti dovra' soddisfare. Quando proviamo ad istanziare il template, il compilatore "tenta" l'istanziazione, tirando ovviamente in ballo conversioni built-in, user-defined, ecc, e se l'istanziazione fallisce "tenta", nei limiti del possibile, di emettere un messaggio di errore comprensibile.
Nel caso concreto di cui sopra, il compilatore trovava un operatore di somma templatizzato (in un header incluso indirettamente) ed in mancanza di altro cercava di applicarlo alla mia espressione (errata), contribuendo solo a peggiorare la situazione.

Migliorare i messaggi di errore?
Il problema di cui sopra e' ormai noto nella comunita' degli sviluppatori, e piu' di uno ha "reinventato" una soluzione (molto parziale) basata sull'uso di typedef. Esiste anche una soluzione alternativa, dall'uso piu' ristretto, basata sulla metaprogrammazione template. In entrambi i casi, l'obiettivo e' quello di rendere piu' chiaro il messaggio di errore emesso dal compilatore, quando erroneamente utilizza un template in contesti in cui non dovrebbe considerarlo.
Premesso che l'approccio che intendo presentare in questo articolo e' molto diverso (sia nella realizzazione che nei principi), credo sia utile rivedere brevemente queste tecniche su un semplice esempio, a beneficio di chi non si e' ancora scontrato con il problema.

Supponiamo di voler realizzare tre classi A, B, C, totalmente scorrelate tra loro, tranne per una caratteristica comune: la possibilita' di spedire i loro dati su un canale di Inter-Process Communication (es. pipe o socket).
Una possibilita' sarebbe quella di derivare A, B e C da una classe base comune, che definisca un metodo virtuale Send; questo sarebbe probabilmente il percorso seguito da chiunque apprezzi le possibilita' offerte dal paradigma object oriented.
Pensiamo pero' ad una situazione in cui si vogliano rendere A, B e C "piccole e veloci", e dove quindi anche l'overhead del puntatore alla tavola delle funzioni virtuali sia ritenuto, a torto o a ragione, inaccettabile. Potremmo allora definire il metodo Send in ognuna di esse:

class IPC
  {
  //....
  } ;

class A
  {
  public :
    IPC& Send( IPC& ) const ;
    //....
  } ;

class B
  {
  public :
    IPC& Send( IPC& ) const ;
    //....
  } ;

class C
  {
  public :
    IPC& Send( IPC& ) const ;
    //....
  } ;
A questo punto, volendo utilizzare l'usuale operator << per inviare oggetti di classe A, B o C su un canale di IPC, e volendo evitare di scriverne tre versioni sostanzialmente identiche (a parte il tipo del parametro), potremmo definire un template operator << come segue:
template< class T > IPC& operator <<( IPC& i, const T& t )
  {
  return( t.Send( i ) ) ;
  }

Avremmo quindi la possibilita' di scrivere codice come il seguente:
  A a ;
  B b ;
  C c ;
  IPC i ;

  i << a << b << c ;
senza alcun overhead, ne' di spazio ne' di tempo, introdotto dai metodi virtuali.

Rimane pero' almeno un problema nelle poche righe di codice viste sopra: possono causare errori di compilazione un po' strani.
Pensiamo ad una situazione in cui, nella stessa unita' di compilazione in cui utilizziamo A, B e C come sopra, si trovino le righe seguenti:

class SomeOtherStuff
  {
  } ;

// piu' avanti...

SomeOtherStuff s ;
i << s ;
Ovviamente ci siamo sbagliati, e probabilmente volevamo utilizzare un'altra variabile anziche' i (o s), oppure (come parte di un codice piu' ampio) volevamo scrivere < ed abbiamo invece scritto <<, o che altro.
Il compilatore, comunque, deve fare il suo dovere: provare ad istanziare il template operator << con T = SomeOtherStuff. L'errore risultante non sara' dei migliori; probabilmente, il compilatore emettera' un messaggio del tipo "Send is not a member of SomeOtherStuff".

Come al solito, visto per gradi e su un esempio minimale si tratta di un errore facile da capire; quando coinvolge header di libreria piuttosto massicci (e spesso scritti con uno stile non molto leggibile), e soprattutto quando non ci aspettiamo sorprese del genere, il messaggio puo' deviare l'attenzione del programmatore su elementi del tutto estranei alla risoluzione del problema.

Realizzando che tutto questo avviene proprio a causa del modello implicito dei vincoli, molti programmatori hanno trovato una semplice scappatoia, consistente nell'esplicitare i vincoli con alcune tecniche, prima fra tutte l'uso dei typedef.
Nel nostro caso, potremmo trasformare il codice come segue:
class A
  {
  public :
    typedef void CanBeSentToIPC ;
    IPC& Send( IPC& ) const ;
    //....
  } ;

class B
  {
  public :
    typedef void CanBeSentToIPC ;
    IPC& Send( IPC& ) const ;
    //....
  } ;

class C
  {
  public :
    typedef void CanBeSentToIPC ;
    IPC& Send( IPC& ) const ;
    //....
  } ;

template< class T > IPC& operator <<( IPC& i, const T& t )
  {
  typedef typename T::CanBeSentToIPC Constraint1 ;

  return( t.Send( i ) ) ;
  }
In pratica, ogni classe che puo' essere spedita su un canale di IPC "dichiara" questa caratteristica attraverso un typedef. Il template operator << "verifica" la presenza della dichiarazione attraverso un ulteriore typedef.
Con questa implementazione, il nostro codice (errato) di cui sopra generera', di solito, un messaggio di errore ritenuto da molti piu' leggibile, come il seguente: "no type named CanBeSentToIPC in class SomeOtherStuff".
Ovviamente, giocando con il typedef possiamo spingere il compilatore ad emettere un messaggio piu' o meno comprensibile, soprattutto se abbiamo in mente una piattaforma di riferimento.
Come vi accennavo, esiste anche una soluzione alternativa (che qui non vedremo) per esprimere il vincolo di ereditarieta' senza richiedere di inserire dei typedef nelle varie classi.

Perche' non basta
In molti casi potremmo accontentarci di quanto sopra. Io stesso ho fatto uso di queste tecniche, sia in casi reali che in articoli vari (si veda ad es. [3]).
Possiamo quindi dichiarare il problema come "superato"? A mio avviso no, per due buone ragioni.

La prima ragione e' che non sempre il nostro messaggio di errore "artificiale" e' cosi' leggibile come crediamo, soprattutto per i non-esperti. Questo e' un problema serio, soprattutto per chi scrive librerie destinate ad un ampio numero di programmatori.

La seconda ragione e' che stiamo ignorando il problema anziche' risolverlo. Quando abbiamo scritto le classi A, B, C ed il template operator <<, noi avremmo voluto dire "applicalo solo alle classi A, B e C (almeno per ora)". Invece ci siamo accontentati di dire "applicalo per qualunque classe, e dai un messaggio di errore se non ci riesci".
Possiamo illuderci che sia tutto sommato la stessa cosa, ma non e' cosi': avere un template operator << "pigliatutto" puo' causare la sua buona dose di problemi. Vediamo un semplice esempio, ottenuto modificando leggermente il codice dato sopra. Supponiamo di voler spedire anche i numeri interi sul canale di IPC, e di definire quindi un apposito operator <<:
IPC& operator <<( IPC& i, int t )
  {
  // ...
  return( i ) ;
  }

A questo punto possiamo illuderci di poter scrivere, oltre a codice come:
i << 1 ;

che ovviamente compila, anche codice come:
i << 'a' ;

che dovrebbe compilare (dopo la promozione da char ad int) ma che invece genera un messaggio di errore, perche' il compilatore prova ad applicare il template pigliatutto, che e' un miglior candidato in quanto non richiede promozione.

Non si tratta quindi solo di un problema di messaggi di errore piu' o meno leggibili: lo stile implicito dei template si porta dietro altri problemi, che buona parte della comunita' C++ ha deciso di ignorare, ma che rimangono comunque ben presenti.
Per fortuna, il C++ mette a disposizione strumenti migliori del typedef per risolvere il vero problema, anziche' tentare di rattoppare uno dei suoi sintomi.

Circoscrivere un template
Se torniamo al problema originale, ci rendiamo rapidamente conto che il nostro vero obiettivo non e' quello di scrivere un template "pigliatutto" e generare errori leggibili, ma e' quello di scrivere un template circoscritto ad alcune classi.
In altre parole, noi vorremmo poter dire al compilatore: "ho scritto un template per non scrivere, debuggare e manutenere molte righe fondamentalmente identiche; tuttavia, applicalo solo alle classi A, B e C. Negli altri casi, comportati come se questo template non esistesse".
In questo modo, non avremmo i problemi di cui sopra, perche' scrivendo
i << 'a' ;

il compilatore non proverebbe neppure ad utilizzare il template operator <<, e passerebbe quindi a considerare la promozione char -> int come desiderato.

Come possiamo circoscrivere un template? E gia' che ci siamo, come possiamo circoscriverlo in modo estendibile, permettendo ad esempio ad altri programmatori di aggiungere una classe D che possa essere utilizzata come parametro del template, ovviamente senza modificare il template in questione?
La risposta e' piuttosto semplice, ma fa uso di un aspetto del C++ introdotto in tempi recenti (durante la standardizzazione) e non ancora implementato da tutti i compilatori.
Va peraltro sottolineato che la tecnica che sto per presentare permette di "circoscrivere" template function e template operator, ma non template class. Per queste ultime... troveremo una soluzione in futuro :-).

Koenig Lookup
La chiave di volta per circoscrivere un template e' la cosiddetta Koenig Lookup (che nello standard e' riportata come Argument-Dependent Name Lookup, paragrafo 3.4.2). Non scendero' nei dettagli di tutti i punti relativi; cio' che ci interessa maggiormente e' pero' riportato di seguito: "When an unqualified name is used as the postfix-expression in a function call, other namespaces not considered during the usual unqualified look up may be searched [...] If T is a class type, its associated classes are the class itself and its direct and indirect base classes. Its associated namespaces are the namespaces in which its associated classes are defined".

Cosa significa tutto questo? Significa che le funzioni candidate vengono ricercate non solo tra quelle visibili nel punto di chiamata, ma anche tra altre, presenti ad esempio nel namespace in cui vengono dichiarati i tipi dei parametri attuali usati nella chiamata di funzione (povero parser... :-).
Credo che per capire bene la regola sia utile conoscere *perche'* e' stata introdotta. Molti di voi sapranno che nel C++ standard, tutti gli elementi di libreria appartengono al namespace std. Quindi, per usare ad esempio i numeri complessi (visto che li ho nominati all'inizio...), possiamo scrivere codice come:

#include <complex>
using std::complex ;
// ...
complex< int > a( 1, 2 ) ;
a = a + a ;
Notiamo un punto molto importante: le chiamate di cui sopra potrebbero fare riferimento ad un operator +() dichiarato fuori dal template complex (quindi non come member function), ma che comunque fara' parte del namespace std (come ogni altra funzione definita dallo standard), e per cui non compare alcuna using declaration nel codice dato (che tuttavia compila!).
Come fa, quindi, il compilatore a "trovare" il giusto operator +, che evidentemente e' in un namespace che non lo rende visibile nel punto in cui e' utilizzato? Lo trova con la regola data sopra, cercando anche (in modo trasparente) nel namespace dove e' dichiarato il tipo dei parametri attuali. Il risultato e' esattamente "quello che il programmatore si aspetta"; un esempio di come le cose possano essere complesse sotto il coperchio per risultare piu' naturali in superficie.

Tirando le somme...
Anche se la Koenig Lookup e' nata per rendere piu' semplice e naturale l'utilizzo dei namespace, nulla ci impedisce di usarla, nello spirito di questa rubrica, in modo creativo.
Riscriviamo quindi il nostro codice originale come segue:
class IPC
  {
  //....
  } ;

// User: never place the following namespace
// in a using declaration or directive
namespace CanBeSentToIPC
  {
  class A
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;

  class B
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;

  class C
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;

  template< class T > IPC& operator <<( IPC& i, const T& t )
    {
    return( t.Send( i ) ) ;
    }
  }

using CanBeSentToIPC::A ;
using CanBeSentToIPC::B ;
using CanBeSentToIPC::C ;

In pratica, inseriamo le classi A, B e C in un namespace "ausiliario" che ho chiamato CanBeSentToIPC. Nello stesso namespace, dichiariamo il nostro template operator <<. Dopodiche' rendiamo visibili le classi A, B e C ma non il template operator <<.

Le conseguenze di questo, se avete seguito quanto sopra, non dovrebbero sorprendervi. Se scriviamo codice lecito, come il seguente:

  A a ;
  B b ;
  C c ;
  IPC i ;

  i << a << b << c ;
Tutto funziona a dovere, perche' il compilatore cerca un operator << anche nel namespace in cui sono stati dichiarati i tipi dei parametri (quindi anche dentro CanBeSentToIPC).
Se invece scriviamo codice come:
SomeOtherStuff s ;
i << s ;

Otteniamo un messaggio molto chiaro, del tipo "no match for IPC& << SomeOtherStuff&", che e' esattamente quello che otterremmo se (nella situazione iniziale) non avessimo usato un template e avessimo invece scritto tre operatori indipendenti.

Infine, se passiamo alla situazione problematica vista sopra, aggiungendo le seguenti righe:

IPC& operator <<( IPC& i, int t )
  {
  // ...
  return( i ) ;
  }

//...

i << 'a' ;
Vedremo che tutto compila a dovere, perche' il compilatore non tentera' neppure di utilizzare il template operator <<, in quando il tipo del parametro (char) non lo porta a ricercare le funzioni in altri namespace.

Estendibilita' ed altre considerazioni
Vediamo ora come dovrebbe operare chiunque desideri estendere le classi "spedibili" via IPC.
Ne approfittero' anche per ripulire un poco il codice visto sopra, che apparentemente lega tra loro le tre classi ed il template operator <<. In realta', una versione "seria" del codice utilizzerebbe un'altra caratteristica fondamentale dei namespace, ovvero quella di essere "aperti" e di poter essere quindi arricchiti in unita' di compilazione diverse (a differenza delle classi).
Avremmo quindi codice come:
// IPC.H

class IPC
  {
  //....
  } ;

// User: never place the following namespace
// in a using declaration or directive
namespace CanBeSentToIPC
  {
  template< class T > IPC& operator <<( IPC& i, const T& t )
    {
    return( t.Send( i ) ) ;
    }
  }


// A.H

#include "IPC.H"

// User: never place the following namespace
// in a using declaration or directive
namespace CanBeSentToIPC
  {
  class A
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;
  }
using CanBeSentToIPC::A ;


// B.H 

#include "IPC.H"

// User: never place the following namespace
// in a using declaration or directive
namespace CanBeSentToIPC
  {
  class B
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;
  }
using CanBeSentToIPC::B ;


// C.H

#include "IPC.H"

// User: never place the following namespace
// in a using declaration or directive
namespace CanBeSentToIPC
  {
  class C
    {
    public :
      IPC& Send( IPC& ) const ;
      //....
    } ;
  }
using CanBeSentToIPC::C ;
Credo risulti ovvio che per scrivere una nuova classe (es. D) che possa essere spedita su IPC, dobbiamo operare esattamente come abbiamo fatto per A, B, o C: introdurla nel giusto namespace, dotarla del metodo Send (che fa parte dei vincoli impliciti del template operator <<) e renderla visibile ai suoi utilizzatori con una using declaration. Un'ultima considerazione: in quanto sopra, per semplicita', ho assunto che si volessero comunque dichiarare le classi A, B e C nel namespace globale. Se invece si intende dichiararle in un namespace differente, la tecnica non cambia: avremo due namespace innestati, con le classi dichiarate nel piu' interno e poi esposte in quello esterno, e con il template operator << dichiarato nel piu' interno e non esposto in alcun modo.

Conclusioni
Ancora una volta, siamo partiti da un problema frequente ed apparentemente di difficile soluzione. Abbiamo poi visto come, usando in modo creativo le funzionalita' di base del linguaggio, si possano ottenere risultati interessanti, senza dubbio migliori degli approcci piu' tradizionali.
Il risultato finale, come ripeto spesso, e' un codice piu' sicuro da utilizzare anche se un po' piu' complesso da scrivere. Un trade-off che deve sempre essere tenuto ben presente da chiunque scriva codice destinato ad essere riutilizzato da un buon numero di programmatori.

Bibliografia
[1] Carlo Pescio, "Programmazione ad Oggetti e Programmazione Generica", Computer Programming No. 62.
[2] Carlo Pescio, "Ipse Dixit", Computer Programming No. 64.
[3] Carlo Pescio, "Oggetti e Quantita'", Computer Programming No. 72.

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.