2.8 – Naming collisioni e un’introduzione a namespaces

Diciamo che si sta guidando a casa di un amico per la prima volta, e l’indirizzo dato a voi è 245 Front Street a Mill City. Dopo aver raggiunto Mill City, si tira la mappa, solo per scoprire che Mill City ha in realtà due diverse strade davanti in tutta la città gli uni dagli altri! A quale andresti? A meno che non ci fosse qualche indizio aggiuntivo per aiutarti a decidere (ad esempio ti ricordi che la sua casa è vicino al fiume) dovresti chiamare il tuo amico e chiedere maggiori informazioni. Poiché questo sarebbe confuso e inefficiente (in particolare per il tuo postino), nella maggior parte dei paesi, tutti i nomi delle strade e gli indirizzi di casa all’interno di una città devono essere unici.

Allo stesso modo, C++ richiede che tutti gli identificatori non siano ambigui. Se due identificatori identici vengono introdotti nello stesso programma in modo che il compilatore o il linker non possano distinguerli, il compilatore o il linker produrranno un errore. Questo errore viene generalmente definito collisione di denominazione (o conflitto di denominazione).

Un esempio di una denominazione di collisione

a.cpp:

1
2
3
4
5
6

#include <iostream>
void myFcn(int x)
{
std::cout << x;
}

principale.cpp:

1
2
3
4
5
6
7
8
9
10
11

#include <iostream>
void myFcn(int x)
{
std::cout << 2 * x;
}
int main()
{
ritorno 0;
}

Quando il compilatore compila questo programma, compilare a.cpp e il principale.cpp in modo indipendente, e ogni file verrà compilato senza problemi.

Tuttavia, quando il linker viene eseguito, collegherà tutte le definizioni in a.cpp e principale.cpp insieme, e scoprire definizioni contrastanti per la funzione myFcn. Il linker verrà quindi interrotto con un errore. Si noti che questo errore si verifica anche se myFcn non viene mai chiamato!

La maggior parte delle collisioni di denominazione si verificano in due casi:
1) Due (o più) definizioni per una funzione (o variabile globale) vengono introdotte in file separati che vengono compilati nello stesso programma. Ciò si tradurrà in un errore di linker, come mostrato sopra.
2) Due (o più) definizioni per una funzione (o variabile globale) vengono introdotte nello stesso file (spesso tramite un #include). Ciò si tradurrà in un errore del compilatore.

Man mano che i programmi diventano più grandi e usano più identificatori, le probabilità che venga introdotta una collisione di nomi aumentano in modo significativo. La buona notizia è che C++ fornisce un sacco di meccanismi per evitare collisioni di denominazione. L’ambito locale, che mantiene le variabili locali definite all’interno delle funzioni in conflitto tra loro, è uno di questi meccanismi. Ma l’ambito locale non funziona per i nomi delle funzioni. Quindi, come facciamo a mantenere i nomi delle funzioni in conflitto tra loro?

Che cos’è uno spazio dei nomi?

Torna al nostro indirizzo analogia per un momento, avere due strade anteriori era problematico solo perché quelle strade esistevano all’interno della stessa città. D’altra parte, se dovessi consegnare la posta a due indirizzi, uno al 209 Front Street a Mill City e un altro indirizzo al 417 Front Street a Jonesville, non ci sarebbe confusione su dove andare. In altre parole, le città forniscono raggruppamenti che ci permettono di disambiguare indirizzi che altrimenti potrebbero entrare in conflitto tra loro. Gli spazi dei nomi si comportano come le città in questa analogia.

Uno spazio dei nomi è una regione che consente di dichiarare nomi al suo interno allo scopo di disambiguazione. Lo spazio dei nomi fornisce un ambito (chiamato ambito dello spazio dei nomi) ai nomi dichiarati al suo interno, il che significa semplicemente che qualsiasi nome dichiarato all’interno dello spazio dei nomi non verrà scambiato per nomi identici in altri ambiti.

Key insight

Un nome dichiarato in uno spazio dei nomi non verrà scambiato per un nome identico dichiarato in un altro ambito.

All’interno di uno spazio dei nomi, tutti i nomi devono essere univoci, altrimenti si verificherà una collisione dei nomi.

Gli spazi dei nomi vengono spesso utilizzati per raggruppare gli identificatori correlati in un progetto di grandi dimensioni per garantire che non si scontrino inavvertitamente con altri identificatori. Ad esempio, se metti tutte le tue funzioni matematiche in uno spazio dei nomi chiamato math, le tue funzioni matematiche non entreranno in collisione con funzioni con nome identico al di fuori dello spazio dei nomi matematici.

Parleremo di come creare i propri spazi dei nomi in una lezione futura.

Lo spazio dei nomi globale

In C++, qualsiasi nome non definito all’interno di una classe, una funzione o uno spazio dei nomi è considerato parte di uno spazio dei nomi definito implicitamente chiamato spazio dei nomi globale (a volte chiamato anche ambito globale).

Nell’esempio nella parte superiore della lezione, le funzioni main() ed entrambe le versioni di myFcn() sono definite all’interno dello spazio dei nomi globale. La collisione dei nomi riscontrata nell’esempio si verifica perché entrambe le versioni di myFcn() finiscono all’interno dello spazio dei nomi globale, il che viola la regola che tutti i nomi nello spazio dei nomi devono essere univoci.

Lo spazio dei nomi std

Quando C++ è stato originariamente progettato, tutti gli identificatori nella libreria standard C++ (inclusi std::cin e std::cout) erano disponibili per essere utilizzati senza il prefisso std:: (facevano parte dello spazio dei nomi globale). Tuttavia, ciò significava che qualsiasi identificatore nella libreria standard poteva potenzialmente entrare in conflitto con qualsiasi nome scelto per i propri identificatori (definito anche nello spazio dei nomi globale). Il codice che funzionava potrebbe improvvisamente avere un conflitto di denominazione quando hai #incluso un nuovo file dalla libreria standard. O peggio, i programmi che compilerebbero sotto una versione di C++ potrebbero non essere compilati sotto una versione futura di C++, poiché i nuovi identificatori introdotti nella libreria standard potrebbero avere un conflitto di denominazione con il codice già scritto. Quindi C++ ha spostato tutte le funzionalità della libreria standard in uno spazio dei nomi denominato “std” (abbreviazione di standard).

Si scopre che il nome di std::cout non è realmente std:: cout. In realtà è solo cout, e std è il nome dello spazio dei nomi di cui fa parte identifier cout. Poiché cout è definito nello spazio dei nomi std, il nome cout non sarà in conflitto con oggetti o funzioni denominate cout che creiamo nello spazio dei nomi globale.

Allo stesso modo, quando si accede a un identificatore definito in uno spazio dei nomi (ad esempio std::cout) , è necessario dire al compilatore che stiamo cercando un identificatore definito all’interno dello spazio dei nomi (std).

Key insight

Quando si utilizza un identificatore definito all’interno di uno spazio dei nomi (come lo spazio dei nomi std), è necessario dire al compilatore che l’identificatore si trova all’interno dello spazio dei nomi.

Ci sono alcuni modi diversi per farlo.

Qualificatore dello spazio dei nomi esplicito std::

Il modo più semplice per dire al compilatore che vogliamo usare cout dallo spazio dei nomi std è usando esplicitamente il prefisso std::. Per esempio:

1
2
3
4
5
6
7

#include <iostream>
int main()
{
std::cout << “Ciao mondo!”; // quando diciamo cout, intendiamo il cout definito nello spazio dei nomi std
return 0;
}

Il simbolo:: è un operatore chiamato operatore di risoluzione dell’ambito. L’identificatore a sinistra del simbolo :: identifica lo spazio dei nomi in cui è contenuto il nome a destra del simbolo::. Se non viene fornito alcun identificatore a sinistra del simbolo::, viene assunto lo spazio dei nomi globale.

Quindi quando diciamo std::cout, stiamo dicendo “il cout che vive nello spazio dei nomi std”.

Questo è il modo più sicuro per usare cout, perché non c’è ambiguità su quale cout stiamo facendo riferimento (quello nello spazio dei nomi std).

Best practice

Utilizzare prefissi di spazi dei nomi espliciti per accedere agli identificatori definiti in uno spazio dei nomi.

Usando lo spazio dei nomi std (e perché evitarlo)

Un altro modo per accedere agli identificatori all’interno di uno spazio dei nomi è usare un’istruzione using directive. Ecco il nostro programma originale “Hello world” con una direttiva using:

1
2
3
4
5
6
7
8
9

#include <iostream>
using namespace std; // questa è una direttiva using dicendo al compilatore di verificare il namespace std durante la risoluzione di identificatori senza prefisso
int main()
{
cout << “Ciao mondo!”; // cout non ha prefisso, quindi il compilatore controllerà se cout è definito localmente o nello spazio dei nomi std
restituisce 0;
}

Una direttiva using indica al compilatore di controllare uno spazio dei nomi specificato quando si tenta di risolvere un identificatore che non ha prefisso dello spazio dei nomi. Quindi, nell’esempio precedente, quando il compilatore va a determinare quale identificatore è cout, controllerà sia localmente (dove non è definito) che nello spazio dei nomi std (dove corrisponderà a std::cout).

Molti testi, tutorial e persino alcuni compilatori raccomandano o usano una direttiva using nella parte superiore del programma. Tuttavia, utilizzato in questo modo, questa è una cattiva pratica, e altamente scoraggiato.

Considera il seguente programma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#include <iostream> // importa la dichiarazione di std::cout
using namespace std; // rende std::cout accessibile come “cout”
int cout() // dichiara il proprio “cout” funzione
{
ritorno 5;
}
int principale()
{
cout < < ” Ciao, mondo!”; / / Errore di compilazione! Quale cout vogliamo qui? Quello nello spazio dei nomi std o quello che abbiamo definito sopra?
ritorno 0;
}

Il programma di cui sopra non viene compilato, perché il compilatore ora non può dire se vogliamo la funzione cout che abbiamo definito, o il cout che è definito all’interno dello spazio dei nomi std.

Quando si utilizza una direttiva using in questo modo, qualsiasi identificatore che definiamo può entrare in conflitto con qualsiasi identificatore con nome identico nello spazio dei nomi std. Ancora peggio, mentre un nome identificativo potrebbe non essere in conflitto oggi, potrebbe essere in conflitto con nuovi identificatori aggiunti allo spazio dei nomi std nelle future revisioni della lingua. Questo era il punto di spostare tutti gli identificatori nella libreria standard nello spazio dei nomi std in primo luogo!

Warning

Evita di usare le direttive (come usare lo spazio dei nomi std;) nella parte superiore del tuo programma. Violano il motivo per cui gli spazi dei nomi sono stati aggiunti in primo luogo.

Parleremo di più sull’uso delle istruzioni (e su come utilizzarle in modo responsabile) nella lezione 6.12-Utilizzo delle istruzioni.

2.9 — Introduzione al preprocessore

Indice

2.7 — Programmi con più file di codice

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.