Dr. Carlo Pescio
Template, Friend e Namespace

Pubblicato su C++ Informer No. 9, Settembre/Ottobre 1999

Come ormai saprete, il processo di standardizzazione del C++ ha portato non solo a chiarimenti e precisazioni riguardo la semantica dei diversi costrutti, ma anche ad un consistente potenziamento del linguaggio. In particolare, il sottoinsieme del C++ dedicato al supporto per la programmazione generica (i template) e' stato ampiamente esteso per supportare al meglio le esigenze di STL, la libreria di contenitori ed algoritmi che fa ormai parte della standard library.
Queste estensioni e chiarimenti hanno coinvolto anche l'interazione tra la keyword friend, i template, ed i namespace. In quanto segue mi concentrero' sugli specifici dettagli, dando per scontato il significato di friend (ricordo semplicemente a chi inizia ora a conoscere il linguaggio che friend serve a concedere ad altre classi o funzioni l'accesso ai membri privati e protetti di una classe). E' invece quasi doveroso dedicare un minimo di spazio per giustificarne l'utilita'. Capita frequentemente di leggere, soprattutto nei newsgroup, attacchi di qualche autoproclamato purista che tenta di svilire il C++ "perche' supportando friend diminuisce l'incapsulazione".
Simili affermazioni sono totalmente prive di fondamento: innanzitutto, l'uso del friend e' totalmente opzionale: il C++ non costringe in alcun modo (rispetto ad altri linguaggi come Java) ad usare friend. Ed in realta', l'uso intelligente di friend aumenta l'incapsulazione, perche' permette di esporre alcune funzionalita' in modo piu' selettivo del semplice private/protected/public. Pur non essendo una soluzione completa all'esportazione selettiva dei metodi, il friend e' uno strumento utile che completa la gamma di funzionalita' di un linguaggio ricco come il C++. Consiglio semplicemente a chi non e' convinto di considerare il grado di incapsulazione e type safety ottenibile implementando due famosi pattern (Memento ed Iterator, si veda [1]) con e senza l'uso di friend.

Non-template friend di una classe template
Il caso piu' semplice di utilizzo del friend in un template si ha quando una classe template vuole dichiarare un'altra classe non-template (la cui dichiarazione sia gia' apparsa nell'unita' di compilazione, ad es. tramite un #include) come friend.
In questo caso utilizziamo la consueta notazione:
// ... qui appare la dichiarazione della classe A ...

template< class T > class B
  {
  friend class A ;
  // ...
  // esempio di dato privato:
  int x ;
  } ;

Questo significa che la classe A avra' diritto di accedere ai membri privati e protetti di ogni istanza del template B. Notiamo che pur apparendo all'interno di un template, la classe A non viene considerata come parametrizzata sul tipo T. Vediamo un esempio; dichiarando A come segue:

class A
  {
  public :
    void f1( B< int > b1 ) ;
    void f2( B< double > b2 ) ;
  } ;
sia A::f1 che A::f2 avranno accesso alla parte privata di b1 e b2, in quanto entrambi i parametri hanno come classe un'istanza di B.
Possiamo ovviamente dichiarare uno o piu' metodi di A come friend di B. In questo caso, e' necessario che la dichiarazione di B sia stata preceduta dalla dichiarazione completa di A, non da una semplice forward declaration.

Template friend di una template class, con binding uniforme
Questo caso corrisponde alla situazione in cui, anziche' voler dichiarare una non-template class A come friend di B, vogliamo dichiarare una template class C come friend di B, assumendo un binding uniforme dei parametri. In altre parole, significa che B< int > avra' come friend C< int > ma non C< double >, e cosi' via.
In questo caso utilizziamo una semplice variante della sintassi precedente:
// ... qui appare la dichiarazione della classe template C ...

template< class T > class B
  {
  friend class C< T > ;
  // ...
  // esempio di dato privato:
  int x ;
  } ;

A differenza del caso precedente, ora la classe C non ha accesso alla parte privata di ogni istanza di B, ma solo delle istanze con tipo uniforme. Vediamo un semplice esempio, dove la classe C e' dichiarata come segue:

template< class T > class C
  {
  public :
    void f1( B< T > b1 ) ;
    void f2( B< double > b2 ) ;
  } ;
In questo caso, C<int>::f1 avra' accesso ai dati privati di b1, ma C<int>::f2 non avra' accesso ai dati privati di b2, in quanto il tipo dei parametri non e' corrispondente. Viceversa, sia C<double>::f1 che C<double>::f2 avranno accesso ai dati privati di b1 e b2, di tipo B<double>.
Compilatori recenti, come il diffuso Visual C++ 6, sono in grado di accettare questa sintassi e di fare le verifiche del caso. Anche la versione 2.95 di GCC (ora fuso con EGCS) la accetta senza problemi.

Template friend di una template class, con binding non uniforme
Una estensione del caso precedente riguarda la possibilita' di dichiarare un template friend di un altro, rilassando pero' il vincolo sul binding uniforme dei parametri. Questo richiede una diversa sintassi, non ancora accettata da alcuni compilatori. Vediamo subito un esempio, ricalcato sul precedente:

// ... qui appare la dichiarazione della classe template C ...

template< class T > class B
  {
  template< class T1 > friend class C ;
  // ...
  // esempio di dato privato:
  int x ;
  } ;
In questo caso, ogni istanza del template C, nonche' di ogni sua specializzazione parziale o totale, e' friend della classe B. Di conseguenza, se dichiariamo C come nel caso precedente:

template< class T > class C
  {
  public :
    void f1( B< T > b1 ) ;
    void f2( B< double > b2 ) ;
  } ;
Sia in C< int > che in C< double > (che in qualunque altro C< X >) sia f1 che f2 avranno accesso ai dati privati del loro parametro.
Tra i compilatori che non supportano questa possibilita' troviamo il Visual C++ 6, mentre il buon GCC 2.95 non ha problemi nell'accettare il codice dato sopra.
E' importante notare che la sintassi precedente si applica anche al caso in cui B non e' un template, e in cui si voglia semplicemente dichiarare una intera famiglia di classi (ovvero una template class) come friend di una non-template class.

Template specialization friend di una template class
In questo caso ricadiamo in una situazione gia' vista, oppure in una situazione illegale. In particolare, se vogliamo dichiarare una specializzazione totale di un template come friend di una classe (template o meno), ci troviamo in un caso del tutto analogo al primo. Ad esempio, nel seguente listato:

// ... qui appare la dichiarazione della classe template C ...

template< class T > class B
  {
  friend class C< int > ;
  // ...
  // esempio di dato privato:
  int x ;
  } ;

solo C< int > avra' accesso ai membri privati di B.

Viceversa, se tentiamo una specializzazione parziale:

// ... qui appare la dichiarazione della classe template C ...

template< class T > class B
  {
  friend class C< T* > ;
  // ...
  // esempio di dato privato:
  int x ;
  } ;

il codice e' illegale. Lo stesso avviene per un caso di binding non uniforme.

Forward declaration
Sinora ho assunto che, nel punto in cui una classe veniva dichiarata come friend, la dichiarazione della stessa classe fosse gia' apparsa nell'unita' di compilazione coinvolta.
Cosa succede, invece, se dichiariamo la classe come friend senza che questa sia precedentemente apparsa, come in:

// inizio dell'unita' di compilazione

class A
  {
  friend class B ;
  } ;
Il problema e' indipendente dalla natura di A (che sia un template o una normale classe), e riguarda invece l'interazione tra friend ed i namespace. In particolare, in fase di standardizzazione e' stato discusso se il costrutto precedente fosse lecito, ed in questo caso se la classe B venisse inserita nel namespace globale, nel namespace contenente A oppure dovesse essere considerata una nested class di A. Ovviamente, occorreva tenere ben presenti le esigenze di compatibilita' all'indietro con codice esistente, che ignorava i namespace ma faceva uso di codice del tutto analogo. Occorreva pero' tenere ben presenti anche le interazioni con in nuovi costrutti aggiunti al linguaggio.
La risposta del comitato di standardizzazione e' stata quasi :-) semplice: la classe B viene assunta (non iniettata!) come appartenente al namespace piu' piccolo (ossia piu' interno) tra quelli che contengono A, e che non siano class namespace.
Vediamo un esempio:
namespace Pippo
  {
  namespace Pluto
    {
    class X
      {
      class Y
        {
        friend class B  // significa friend class Pippo::Pluto::B
        } ;
      } ;
    }
  }

Naturalmente, possiamo sempre forzare un namespace specifico usando il nome completamente qualificato per la classe a cui garantiamo la friendship.
La scelta del comitato di standardizzazione e' stata molto ragionevole: prima dello standard, ed in assenza dei namespace, una dichiarazione come la precedente (ovviamente senza Pippo e Pluto) avrebbe inserito B nel namespace globale. Per preservare il codice esistente, il significato non poteva cambiare.
D'altra parte, se la dichiarazione avviene all'interno di un namespace, non e' ragionevole che vada a sporcare il namespace globale, da cui la decisione finale del comitato. Ricordate che anche una local class (dichiarata all'interno di una funzione) appartiene ad un non-class namespace, quindi una forward declaration in una local class viene assunta nel namespace della funzione che contiene la local declaration.

Friend nested class
A questo punto nasce un problema: come si scrive una forward declaration di una friend nested class? Il problema non e' accademico, nel senso che le nested class sono spesso implementate inline, e se devono essere friend occorre spesso una forward declaration.
Un nota importante: molti pensano che le classi nested siano "automaticamente" friend della classe che le contiene; c'e' un certo fondamento in questa assunzione (cosi' come le member function hanno accesso ai dati privati, si puo' pensare che anche una "member class" lo abbia). Non credo valga la pena di discutere su quale soluzione sia la piu' ragionevole: semplicemente, in ANSI/ISO C++ una nested class non e' automaticamente friend della classe esterna.
La sintassi della forward declaration di una nested class (che ho peraltro usato in un precedente numero di C++ Informer) e' la seguente:

class A
  {
  class B ; // forward-declaration di nested class
  friend class B ; // dichiara la precedente classe come friend
  // ... altro, inclusa la dichiarazione di B
  } ;
La differenza fondamentale sta nell'apparentemente ridondante dichiarazione "class B", che serve invece ad inserire B nel namespace di A, ed a far si che la successiva dichiarazione friend trovi B durante il name lookup.

Un dettaglio minore
Una "curiosita'" sul friend (che dimostra anche il livello di dettaglio a cui occorre spingersi nella standardizzazione del linguaggio) e' che la friendship non e' considerata nella sezione di definizione delle classi base. Un esempio chiarira' meglio cosa significa:

class A ;

class B
  {
  friend class A ;
  class Nested {} ;
  // ...
  } ;

// Illegale
class A : public B::Nested 
  {
  } ; 

// Legale
class A
  {
  void f()
    {
    class Local : public B::Nested 
      {
      } ;
    } 
  } ;
La spiegazione e' molto semplice. Dichiarare la classe A come friend all'interno di B significa che ogni membro di A ha accesso alle parti private e protette di B. Tuttavia, la dichiarazione di una classe base per A non e' un membro di A, quindi non ha accesso al tipo privato B::Nested. Viceversa, all'interno (ad es.) di un metodo di A possiamo tranquillamente accedere a B::Nested. Nessuno dei compilatori che ho provato segnala la situazione di illegalita' nel codice dato sopra, a riprova che non basta provare con qualche compilatore per essere certi dell'aderenza allo standard del proprio codice.

Conclusioni
Una puntata tutto sommato semplice, anche se un po' "terminologica", dedicata ad un aspetto di dettaglio ma comunque importante. L'unico elemento che forse sara' risultato un po' estraneo ad alcuni lettori e' il termine "namespace", e la corrispondente keyword nell'esempio dato sopra. Il mini-sondaggio del mese e' quindi: chi di voi usa realmente i namespace nei loro programmi (al di la' di "using namespace std" :-)? Come si trova (pregi e difetti)? Quanti vorrebbero una puntata di ANSI/ISO C++ dedicata ai namespace, magari con raccomandazioni tipo "C++ Manuale di Stile" sul loro utilizzo? Mandatemi qualche riga via email a informer@eptacom.net

Bibliografia
[1] Gamma ed altri, "Design Patterns", Addison-Wesley, 1994.

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.