Dr. Carlo Pescio
Overloading sul tipo del risultato

Pubblicato su C++ Informer No 7, Aprile 1999

Il C++ non consente di definire due o piu' funzioni overloaded con lo stesso nome, stessi parametri, ma tipo diverso per il risultato. Si tratta di un limite "by design", spiegato brevemente da Stroustrup in [1] al paragrafo 7.4.1.
Secondo Stroustrup, proibire l'overloading sul tipo del risultato serve a mantenere una chiamata di funzione context-independent, ovvero, in caso contrario non sarebbe sufficiente osservare la chiamata in se' per capire quale funzione viene chiamata, ma occorrerebbe guardare anche l'utilizzo del risultato, che partecipereebbe a pieno diritto nell'overloading resolution.
Personalmente il guadagno non mi sembra commisurato alla rinuncia, anche perche' simili argomentazioni sono state portate da altri per giungere alla conclusione che l'overloading andrebbe eliminato del tutto.
E' peraltro abbastanza semplice trovare qualche esempio in cui l'overloading sul tipo del risultato potrebbe risultare comodo ed allo stesso tempo portare a codice piu' leggibile. Vediamo un paio di casi:

1) Stile declaration-is-initialization.
Chi ha letto il mio libro [2] sa che considero buona norma dichiarare le variabili all'ultimo istante possibile (e con lo scope piu' piccolo possibile), tipicamente quando siamo in grado di dare un valore significativo all'atto della dichiarazione. Esiste pero' almeno un caso piuttosto comune in cui non possiamo dare il risultato definitivo: quando usiamo l'operator >> per estrarre la variabile da uno stream:
...
int i ;
cin >> i ;
...

Potremmo pensare di cavarcela definendo una funzione Get come segue:
int Get( std::istream& s )
  {
  int i ; // violazione localizzata !
  s >> i ;
  return( i ) ;
  }

tanto piu' che mentre e' comune utilizzare dei << in cascata, lo e' un po' meno per i >>, dove di solito dobbiamo verificare eventuali errori prima di proseguire. Usare una funzione anziche' l'operator >> consentirebbe anche di dichiarare la variabile da inizializzare come const, nel caso il suo valore non debba piu' essere modificabile dopo la lettura. Cio' non e' possibile dove si usa >>.
L'approccio decade pero' rapidamente se pensiamo di estendere Get su piu' tipi, perche' non possiamo avere un overloading sul tipo del risultato. Avere una GetInt, una GetDouble, ecc porta ai soliti problemi di manutenzione quando si cambia il tipo di una variabile e ci si dimentica di aggiornare la Get corrispondente (e grazie alle conversioni implicite il codice, ora sbagliato, potrebbe rimanere compilabile).

2) Operator []
Un caso piu' interessate mi e' capitato tempo fa scrivendo una libreria per l'accesso ai dati, che si sovrapponesse ad un layer ODBC, o ADO (o che altro) e che permettesse di accedere ai valori in base ai nomi dei campi. Idealmente, volevo che il programmatore potesse scrivere codice come:

// ... apre recordset, posiziona cursori, ecc
DB::Record rec ;
// ...
int q = rec[ "quantity" ] ;
// ...
Date d = rec[ "purchase date" ] ;
// ...
string s = rec[ "description" ] ;
// ...
Dove "quantity", "description" ecc sono i nomi dei campi in una tabella su DB relazionale.
Ovviamente Record::operator[] ha un solo valore per il risultato, quindi il codice di cui sopra non e' cosi' facilmente realizzabile. L'alternativa di usare GetInt, GetDate, ecc rendeva il codice client decisamente piu' prolisso e meno leggibile di quanto avrei voluto, oltre ad avere le stesse fragilita' di manutenzione discusse sopra.

D'altra parte, questo e' lo spazio "No Limits" e non "Lamenti e Pianti". Se il linguaggio non ci da' qualcosa gratis, possiamo sempre costruircelo da soli. L'obiettivo e' di poter scrivere codice client come quello sopra, anche a costo di fare un minimo di fatica in piu' mentre si implementa una libreria.

Un piccolo passo
In un mio intervento ("Template Magic", vagamente in stile "No Limits") al Developer's Forum '97, ho fatto vedere come le comuni regole sull'overloading e tipo del risultato si estendano nel caso di funzioni template con parametri espliciti:

#include <iostream>
#include <string>

template< class T > T Get( std::istream& s )
  {
  T t ; // violazione localizzata !
  s >> t ;
  return( t ) ;
  }

int main()
  {
  using std::cin ;
  using std::cout ;

  const int x = Get< int >( cin ) ;
  double d = Get< double >( cin ) ;
  std::string s = Get< std::string >( cin ) ;

  cout << x << " " << d << " " << s ;
  return( 0 ) ;
  }
Questo codice e' gia' meglio di quanto ipotizzato poco fa: perlomeno, non dobbiamo scriverci a mano una funzione GetInt, una GetDouble, ecc, ma possiamo sfruttare la potenza dei template. Notate che nel codice chiamante (main) possiamo tranquillamente utilizzare lo stile declaration-is-initialization. Rimane il grosso limite di dover specificare esattamente, come parametro esplicito della funzione template, il tipo del risultato atteso: il compilatore non e' in grado di dedurre il tipo di T dai parametri della funzione (ovviamente).
Possiamo pero' modificare questa prima versione, restituendo un proxy che sa fare la conversione "giusta" senza bisogno di aiuto da parte del programmatore.

Un salto in avanti
L'idea di restituire un proxy non e' nuova, ed e' molto potente. Si puo' utilizzare in molte situazioni, ad es. per distinguere l'uso di operator[] per ottenere un left value o un right value. In questo caso, il nostro proxy, restituito da Get, dovrebbe essere in grado di convertirsi implicitamente in ogni tipo, scatenando a quel punto la reale operazione di lettura da stream.
Vediamo una possibile implementazione:
#include <iostream>
#include <string>

class Get_Proxy
  {
  public :
    Get_Proxy( std::istream& s ) : is( s )
      {
      }
    template< class T > operator T()
      {
      T t ;
      is >> t ;
      return( t ) ;
      }
  private :
    std::istream& is ;
  } ;
      

Get_Proxy Get( std::istream& s )
  {
  return( Get_Proxy( s ) ) ;
  }


int main()
  {
  using std::cin ;
  using std::cout ;

  int x = Get( cin ) ;
  double d = Get( cin ) ;
  std::string s = Get( cin ) ;

  cout << x << " " << d << " " << s ;
  cin.get() ;
  return( 0 ) ;
  }

Partiamo dal codice chiamante in main: sono spariti i parametri espliciti al template, in quanto Get e' tornata ad essere una semplice funzione. E' anche decisamente leggibile, anche se il nome Get lascia ampi spazi di miglioramento (il solito "esercizio per il lettore"). L'interfaccia e' anche sicura, nel senso che cambiando il tipo di d da double in (es.) int, verra' chiamata una funzione di lettura di intero, senza problemi di manutenzione. E' anche flessibile, in quanto funziona per ogni tipo su cui e' definito l'operator >> (e che disponga di un costruttore di default e di un costruttore di copia; il perche' e' nuovamente lasciato come semplice esercizio).

Vediamo ora come funziona: quando chiamiamo Get( cin ), non succede granche': viene costruito un oggetto temporaneo di classe Get_Proxy, che "ricorda" lo istream da cui leggere il valore, ma lo stream non viene ancora utilizzato.
Tuttavia, scrivere:
int x = Get( cin ) ;

significa anche tentare di inizializzare un int con un Get_Proxy. In generale, si trattera' di costruire un oggetto di classe T a partire da un Get_Proxy. Supponiamo che un costruttore del genere non esista (sicuramente non esiste per i tipi base e di libreria). Allora il compilatore provera' le conversioni implicite (e non ve ne sono) e quelle user-defined. In effetti Get_Proxy definisce una intera famiglia di conversioni user-defined, attraverso il member template operator T(). In questo caso, verra' chiamata l'istanza del template con T uguale ad int. Verra' quindi letto un intero dallo istream precedentemente salvato, e questo verra' assegnato ad x.
Il tutto funziona benissimo anche con i parametri delle funzioni:
void f( double d )
  {
  std::cout << d ;
  }

// piu' avanti:
f( Get( cin ) ) ;  // legge un double e lo passa ad f.

Pur se applicata ad un caso particolare, la tecnica usata qui ha validita' generale. Prima di discutere le generalizzazioni, pero', e' meglio prendere in considerazione i limiti ed i problemi che la soluzione puo' presentare.

Problemi
Il primo problema che incontriamo e' che il codice di cui sopra non viene capito molto bene da alcuni compilatori, ad esempio dal Visual C++ 6.0 con tanto di service pack 2. In particolare, il VC++ non genera il codice per la chiamata all'operatore di conversione, emette un bel warning di variabile non inizializzata (!) per x e d, e poi si inchioda nella libreria run-time se si prova a lanciare l'eseguibile. Compilatori piu' decenti (anche il free ECGS) compilano il codice senza problemi (l'uso con EGCS puo' richiedervi l'eliminazione delle due righe "using ...").
Va notato che, anche con un compilatore scarsotto, potremmo superare il problema accettando qualche limitazione: ad esempio, se non vogliamo supportare "gratuitamente" qualunque tipo, potremmo definire a mano un operator int(), un operator double(), ecc all'interno di Get_Proxy. Eviteremmo problemi al VC++, perderemmo in generalita', ma continueremmo ad avere, lato client, una sintassi di chiamata identica, proprio come se avessimo l'overloading sul tipo del risultato.
Il secondo problema nasce dalla possibilita', per un utente maligno, di dichiarare i propri Get_Proxy e di usarli per scopi mefistofelici. In parte, possiamo impedire questi abusi dichiarando il costruttore di Get_Proxy privato, e la funzione Get() come friend di Get_Proxy (chi dice che friend indebolisce l'incapsulazione?).
Rimane ugualmente la possibilita' per l'utente di scrivere codice del tipo:
const Get_Proxy& r = Get( cin ) ;
int x = r ;
int y = r ;
// ecc

che non brilla per chiarezza, perche' assegna ad x ed y due valori (probabilmente diversi) letti dallo standard input. Direi che si tratta di un problema risolvibile documentando in modo chiaro come ci si aspetta che venga chiamata la funzione Get; piu' avanti vedremo come, volendo, sia possibile mitigarlo ancora.
Un ulteriore problema e' che chiamate del tipo:
Get( cin ) ;
non fanno nulla, e non abbiamo warning di sorta, anche se probabilmente si tratta di un bug nell'uso di Get. Possiamo controllare a run-time che cio' non avvenga, dotando Get_Proxy di un distruttore e di un flag di conversione. Il flag viene posto a false nel costruttore, a true in operator T(), e verificato come true nel distruttore (se e' false, non c'e' stata conversione). Possiamo anche rendere illegale l'utilizzo di cui sopra (in cui r e' utilizzato piu' volte) aggiungendo una asserzione di flag false all'inizio di operator T(), anche se non e' strettamente necessario. Ancora, si potrebbe obiettare che il codice genera un temporaneo di classe Get_Proxy e quindi e' piu' lento dell'alternativa con >>. Questo e' sicuramente vero con compilatori non eccellenti, anche se un buon compilatore potrebbe espandere tutto il codice in linea ed evitare il temporaneo rispettando la as-is rule. Va detto che in molti casi reali (come >> o come il caso del database citato all'inizio) l'overhead di cui stiamo parlando e' totalmente trascurabile. Infine, rimangono i classici problemi dei proxy: non funzionano bene con chiamate a funzioni template, ed aggiungendo una conversione user-defined ne impediscono una seconda. In entrambi i casi, e' necessario un cast esplicito per forzare la conversione desiderata. Di nuovo, si tratta di un limite accettabile in molti contesti reali.

La tecnica in generale
Come accennavo, la tecnica ha validita' generale, anche se di volta in volta dovremo fare delle varianti implementative. Supponiamo di voler realizzare una famiglia di funzioni g(), con parametri identici ma tipi diversi per i risultati. Per simulare, dal punto di vista del chiamante, la presenza dell'overloading sul tipo del risultato, dobbiamo passare attraverso i seguenti passi: 1) Definire una sola funzione g(), che prende i parametri desiderati e restituisce un oggetto di classe g_Proxy. 2) Creare una classe g_Proxy, che nel costruttore salvi all'interno dei propri data member i parametri di g(), che ovviamente gli passera' g() stessa. 3) Dotare la classe g_Proxy di un operatore di conversione per ogni tipo di risultato ammissibile per la famiglia "originale" g(). Nell'esempio dato sopra, tale insieme e' aperto ed ho quindi utilizzato un operatore di conversione template. Ma in altri casi (es. la libreria di accesso al database) tale insieme e' chiuso, ogni funzione richiede una implementazione custom, e non ci servono quindi elementi avanzati come i member template. Ogni operatore di conversione ha un corpo equivalente alla funzione g() "originale" che sostituisce, con l'unica differenza che i parametri vengono sostituiti dai data member di g_Proxy. Alla tecnica di base si possono aggiungere le accortezze di cui sopra (costruttore privato, flag di conversione con verifica nel distruttore) per evitare un utilizzo anomalo della classe g_Proxy. Ovviamente la classe g_Proxy puo' anche essere "nascosta" in un namespace apposito per evitare di inquinare lo spazio globale dei nomi (o lo stesso namespace che contiene g()). Notate che la tecnica funziona sia per funzioni globali che per member function, nonche' per gli operatori (come [] nel caso della libreria per database di cui sopra).

Conclusioni
Ancora una volta, cio' che a prima vista sembra impossibile si riesce invece ad ottenere con un po' di creativita' e di conoscenza del linguaggio. Cio' che e' importante e' che l'uso di aspetti meno noti non traspaia dalle interfacce delle librerie: l'utilizzatore deve poter scrivere codice semplice, naturale e facilmente manutenibile. In questo caso, l'overloading (per quanto simulato) sul tipo del risultato permette di scrivere codice chiaro che non presta il fianco a problemi di manutenzione, come accadrebbe usando funzioni aventi il tipo del risultato come parte del nome. Eliminare il nome del tipo dal nome delle funzioni apre anche la porta ad un uso piu' "estremo" della programmazione generica. Ma questa e' un'altra storia...

Bibliografia
[1] Bjarne Stroustrup, "The C++ Programming Language, 3rd Edition", Addison-Wesley, 1997.
[2] Carlo Pescio, "C++ Manuale di Stile", Edizioni Infomedia, 1995.

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.