Underscore: cosa e' legale, cosa no, e perche'

Pubblicato su C++ Informer No. 10, Gennaio 2000

Alzi la mano chi non ha mai usato un identificatore con due underscore, ad esempio __PIPPO__, oppure un nome come _PIPPO, magari come guardia per impedire le inclusioni multiple di un file header, e magari perche' ha visto che le librerie del compilatore fanno cosi'.
Adesso abbassi la mano chi lo ha fatto apposta, perche' ama il codice con comportamento imprecisato.
Se avete ancora la mano alzata, dovete leggere questo articolo :-). Se non siete sicuri, forse e' meglio leggerlo ugualmente...

Identificatori riservati, secondo lo standard
Alcuni identificatori sono riservati per il compilatore e l'implementazione delle librerie e non andrebbero utilizzati. Vedremo piu' avanti cosa vuol dire "riservati" e *perche'* ci sia bisogno di riservare degli identificatori. Per ora, cerchiamo di definire con precisione quali identificatori siano riservati.

Lo standard finale (questa parte e' infatti stata spostata rispetto al draft disponibile su internet) documenta gli identificatori riservati al punto 17.4.3.1.2:

- Each name that contains a double underscore (__) or begins with an underscore followed by an uppercase letter is reserved to the implementation for any use.
(ogni nome che contiene un doppio underscore o comincia con un underscore seguito da una maiuscola e' riservato).

- Each name that begins with an underscore is reserved to the implementation for use as a name in the global namespace
(ogni nome che comincia con un underscore e' riservato per l'implementazione per l'uso nel namespace globale).

La nota 165 aggiunge (ma non ci interessa molto, gli affezionati vedano il punto 17.4.3.1/1 per convincersi) che tali nomi sono riservati anche nel namespace ::std.

La prima regola e' estremamente chiara: non possiamo usare alcun nome con un doppio underscore o con un underscore iniziale seguito da una maiuscola. La seconda e' meno ovvia, e ci tornero' sopra piu' avanti considerando vari casi (data member, parametro di funzione, ecc).

Per ora, credo sia piuttosto importante chiarire cosa succede se non rispettiamo queste regole: bisogna infatti rimbalzare indietro di circa trecento pagine nello standard per trovare la risposta (e poi ci si lamenta che lo standard non e' semplice da leggere :-).
Il punto 2.10/2 recita infatti: "In addition, some identifiers are reserved for use by C++ implementations and standard libraries and shall not be used otherwise; no diagnostic is required.".
Questa frase va letta alla luce del punto 1.4/2, che chiarisce: "If a program contains a violation of a rule for which no diagnostic is required, this International Standard places no requirement on implementations with respect to that program".
Ovvero: se violate una delle regole sopra, il vostro compilatore puo' fare cio' che vuole. Ed intendo proprio il compilatore, non il codice generato. Di fronte a quel codice il compilatore puo' generare codice qualunque, rifiutarsi di compilare, distruggere l'universo :-), eccetera, e potra' comunque fregiarsi di un totale rispetto dello standard ANSI/ISO. Nei casi concreti, avremo probabilmente un errore piu' o meno incomprensibile in fase di compilazione o link.

Identificatori riservati, in termini umani :-)
Come gia' accennato sopra, il primo dei due casi previsti e' molto chiaro: non si possono usare doppi underscore all'interno dei nostri nomi, o un identificatore che inizia con underscore seguito da maiuscola, da nessuna parte nel codice.

Vorrei invece dedicare qualche parola in piu' a chiarire cosa significhi "ogni nome che comincia con un underscore e' riservato per l'implementazione per l'uso nel namespace globale", prendendo in esame le varie situazioni. Nel prossimo paragrafo vedremo poi *perche'* sia praticamente indispensabile riservare dei nomi a beneficio delle implementazioni.

L'uso di identificatori che iniziano con underscore, tra l'altro, non e' cosi' inusuale come potrebbe sembrare. In molti casi ho avuto modo di leggere codice del genere:

class C
  {
  public :
    C( int _x )
     {
     x = _x ;
     }
  // ...
  } ;
dove l'underscore viene utilizzato per evitare una "collisione" tra il nome del parametro formale ed il nome di un data member. Viene dunque da chiedersi in quali contesti, esattamente, sia lecito usare un identificatore che inizia per underscore (non seguito ne' da un altro underscore ne' da una maiuscola).

Vediamo quindi le diverse situazioni in cui e' possibile introdurre un identificatore:

a) come identificatore di classe, tipo, variabile funzione, class template o function template nel namespace globale. Ad esempio:

// begin file
int _x ;
a1) come identificatore di classe, tipo, variabile, funzione, class template o function template nel namespace std. Ad esempio:

// begin file
namespace std
  {
  int _x ;
  }
b) come identificatore di classe, tipo, variabile funzione, class template o function template in un namespace diverso da quello globale o da std. Ad esempio:
// begin file
namespace MySpace
  {
  int _x ;
  }

c) come parametro di funzione, definita in un qualunque namespace. Ad esempio:
// begin file
void f( int _x ) ;

d) come parametro di una member function. Ad esempio:
// begin file
class C
  {
  void f( int _x ) ;
  }

e) come data member di una classe (o struttura). Ad esempio:
// begin file
class C
  {
  int _x ;
  }

f) come variabile locale (static o meno). Ad esempio:
// begin file
void f()
  {
  int _x ;
  // ...
  }

g) come macro del preprocessore. Ad esempio:
// begin file
#define _x 42

h) come label per un goto. Ad esempio:
// begin file
void f()
  {
  // ...
   _x :
  // ...
  }

i) come identificatore di un elemento di tipo enumerato. Ad esempio:
// begin file
enum e { _x  } ;

Se ho dimenticato qualche caso, sono certo che qualche lettore si fara' sentire per aiutarmi a completare l'elenco.

A questo punto, possiamo notare che buona parte delle situazioni su descritte sono sicure, ovvero non vanno contro le norme dello standard ANSI/ISO.
Vediamole comunque in sequenza:

I casi a) ed a1) sono evidentemente illegali.
Il caso b) e' invece lecito, proprio perche' si tratta di un altro namespace.
Il caso c) e' lecito, perche' i parametri della funzione non sono nel namespace globale, sia che si tratti di una declaration che di una definition.
Il caso d) e' lecito, come caso particolare di c).
Il caso e) e' lecito, come caso particolare di b)
Il caso f) e' lecito, perche' il local scope non e' parte del namespace globale.
Il caso g) e' illecito. Il preprocessore non conosce il concetto di namespace, ed una definizione di macro ha effetto su tutti i namespace.
Il caso h) e' lecito, perche' le label possono essere introdotte solo in function scope.
Il caso i) puo' essere lecito (se il tipo enumerato "e" viene dichiarato in un namespace diverso da quello globale o std) mentre e' illecito nei casi rimanenti.

Tutto sommato, quindi, le situazioni a rischio sono poche. Tuttavia, non di rado si consiglia di evitare l'underscore iniziale in tutte le situazioni, semplicemente per evitare di dover ricordare le regole di cui sopra e/o di dover prestare troppe cautele in fase di manutenzione (pensate ad una funzione che per qualunque ragione viene spostata nel namespace globale, ed il cui nome infranga a quel punto la regola). Tutto sommato, mi sento di suggerire questa regola semplificata a tutti, anche perche' ci vuole realmente poco a scrivere x_ anziche' _x, e questo ci consente di dormire sonni piu' tranquilli.

Perche' riservare dei nomi?
A questo punto immagino che alcuni di voi si stiano chiedendo *perche'* debbano esservi dei nomi riservati. Lo standard C++, come sempre, non lo dice: si tratta di un documento di specifica, non di giustificazione.
E' comunque piuttosto semplice identificare alcune grandi categorie di problemi che vengono risolti riservando dei nomi a beneficio delle implementazioni.
La prima giustificazione e' la necessita', da parte dei produttori di compilatori, di introdurre delle funzionalita' che eccedano lo standard, pur non inficiando la legalita' di un qualunque programma esistente che sia standard-compliant.
Vediamo un esempio: lo standard C++ non si preoccupa (esplicitamente) di argomenti come le librerie a caricamento dinamico (con potenziale necessita' di dichiarare quali nomi esportare all'esterno) o l'esistenza di piu' thread con tutte le problematiche correlate, come l'esistenza del thread local storage.
Tuttavia lo stesso standard non puo' totalmente ignorare l'esistenza di questi problemi: deve fornire un meccanismo attraverso il quale i produttori di compilatori possano estendere il linguaggio, senza interferire con i programmi standard-compliant.
Citando ad esempio un compilatore che ritengo tra i piu' diffusi, il Visual C++ permette di dichiarare la visibilita' esterna alle DLL, o la allocazione come thread local storage, usando estensioni come __declspec( dllexport ) oppure __declspec( thread ).
Se i nomi contenenti un doppio underscore non fossero riservati per l'implementazione, questa estensione potrebbe assegnare un significato diverso (magari di programma invalido) ad un programma che lo standard reputa corretto.

Notiamo che e' corretto che __declspec inizi con un doppio underscore, perche' puo' essere utilizzato in qualunque namespace. Viceversa, identificatori come dllexport o thread non richiedono la presenza di underscore, purche' il compilatore li tratti come particolari solo all'interno del dichiaratore __declspec. Incredibilmente :-), il Visual C++ si dimostra in questo senso allineato allo standard, compilando senza errori codice come il seguente, che tenta inutilmente di ingannarlo:

int dllexport ;
__declspec( dllexport ) int x ;

int main()
  {
  dllexport = 0 ;
  return dllexport ;
  }
Lo stesso criterio di non interferenza consente di introdurre tipi estesi come __int64 senza inficiare in alcun modo la correttezza o il significato di programmi pre-esistenti e standard-compliant.

La seconda ragione che porta a riservare alcuni nomi e' la necessita' (o l'utilita') di introdurre alcune variabili globali all'interno del codice di libreria. Ad esempio, funzioni C come rand() hanno necessita' di tenere traccia della storia passata, e questo avviene memorizzando uno o piu' dati in variabili globali. Variabili che potrebbero essere dichiarate static (locali al modulo in cui sono definite); in tal modo si precluderebbero tuttavia alcune ottimizzazioni, come l'espansione in linea delle funzioni intrinseche.
D'altro canto, ogni nome di variabile globale visibile in uno dei nostri file potrebbe collidere con un nostro nome globale. Da qui l'utilita' (se non proprio la necessita') di riservare alcuni nomi nel solo namespace globale ad uso delle implementazioni. Notiamo infatti che non vi e' alcun problema di collisione se noi utilizziamo una variabile omonima in un namespace non globale, ammesso che la libreria faccia correttamente uso dell'operatore di risoluzione dello scope (::).

La terza ragione per riservare alcuni nomi va ricercata nella necessita' di poter scrivere gli header file di libreria, sia nella parte di dichiarazione che di definizione (per le funzioni inline ed i template) senza rischiare collisioni con eventuali macro (ma anche altri elementi, come vedremo) definite dall'utente.
Pensiamo ad una implementazione del template (standard) list come il seguente:
template< class T, class A = allocator<T> > class list //...

cosa succede se l'utilizzatore include l'header file relativo dopo aver definito una macro di nome T o A, magari in un altro file incluso? Possono succedere diverse cose, in funzione dell'espansione della macro; il caso piu' probabile, e piu' fortunato, e' un errore di compilazione poco comprensibile.
Notiamo che lo stesso problema emerge se ridefiniamo i nomi "template", "class", "allocator" o "list" attraverso le macro. La prospettiva, tuttavia, e' diversa: i nomi suddetti sono keyword o nomi di libreria riservati e documentati. I nomi dei parametri dei template, cosi' come i nomi dei data member, dei parametri delle funzioni, e quant'altro possa apparire in un header file che sia legato alla specifica implementazione, a priori non sono ne' riservati ne' documentati.
Va altresi' notato che un problema analogo si puo' incontrare anche in assenza di macro; se premettiamo alla dichiarazione della lista vista sopra la seguente linea:
enum e { T } ;
avremo nel migliore dei casi un errore di compilazione.

Lo standard, dovendo in qualche modo garantire il significato di programmi che utilizzano la libreria standard stessa, ha scelto la strada di riservare alcuni nomi per l'implementazione. Cio' spiega perche' molte librerie, correttamente, dichiarino le classi standard in modo simile a quanto segue (preso dal solito Visual C++):

template< class _Ty, class _A = allocator<_Ty> > class list //...
Non si tratta quindi di una perversione dell'autore :-), ma di una misura cautelativa garantita dallo standard. Come vedete, infatti, per riprodurre un errore analogo a quello descritto sopra occorrerebbe introdurre un identificatore di enumerato di nome _Ty nel namespace globale (o std), andando contro alla regola dello standard.

Conclusioni Molti programmatori prendono in gioventu' il brutto vizio :-) di usare guardie di compilazione con il doppio underscore, o che partono con un underscore seguito da una maiuscola, andando a sbirciare negli header del compilatore.
Anche il sottoscritto si e' lasciato scappare errori del genere, addirittura nel Manuale di Stile (di cui dovro' aggiornare l'errata corrige, disponibile online sul mio sito).
Peraltro gli stessi produttori di compilatori sembrano dare una accezione piuttosto elastica al termine "riservato per l'implementazione", ed utilizzano nomi riservati all'interno di librerie come (ad es.) MFC che non fanno parte, secondo lo standard, dell'implementazione (tanto che MFC non protegge i parametri dei template come fa invece la libreria standard).
Ricordarsi le regole viste sopra e' tutto sommato piuttosto semplice (a maggior ragione se si adotta la regola piu' restrittiva ma piu' facile), ed anche adeguarvisi non e' difficile: in fondo, una versione standard-compliant delle macro (illegali) del tipo __THIS_FILE__ o _THIS_FILE_ e' semplicemente THIS_FILE_, con un solo underscore in coda e nessuno in testa.
Un piccolo sforzo, quindi, che tuttavia rende il nostro codice meglio allineato con lo standard, e ci protegge da sorprese sgradite al variare del compilatore utilizzato.

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.