2.8 – Collisions de noms et introduction aux espaces de noms

Disons que vous conduisez chez un ami pour la première fois, et l’adresse qui vous est donnée est 245 Front Street à Mill City. En arrivant à Mill City, vous tirez votre carte, pour découvrir que Mill City a en fait deux rues frontales différentes l’une de l’autre en face de la ville! Où irais-tu ? À moins qu’il n’y ait un indice supplémentaire pour vous aider à décider (par ex. vous vous souvenez que sa maison est près de la rivière) vous devriez appeler votre ami et demander plus d’informations. Parce que cela serait déroutant et inefficace (en particulier pour votre facteur), dans la plupart des pays, tous les noms de rues et adresses de maisons dans une ville doivent être uniques.

De même, C++ exige que tous les identifiants ne soient pas ambigus. Si deux identifiants identiques sont introduits dans le même programme d’une manière que le compilateur ou l’éditeur de liens ne peut pas les distinguer, le compilateur ou l’éditeur de liens produira une erreur. Cette erreur est généralement appelée collision de nommage (ou conflit de nommage).

Un exemple de collision de nommage

a.cpp:

1
2
3
4
5
6

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

principal.rpc:

1
2
3
4
5
6
7
8
9
10
11

# inclure < iostream >
void myFcn(int x)
{
std:: cout < < 2*x;
}
int principal()
{
retour 0;
}

Lorsque le compilateur compile ce programme, il compile a.cpp et principal.cpp indépendamment, et chaque fichier se compilera sans problème.

Cependant, lorsque l’éditeur de liens s’exécute, il liera toutes les définitions dans a.cpp et principal.cpp ensemble, et découvrez des définitions contradictoires pour la fonction myFcn. L’éditeur de liens abandonnera alors avec une erreur. Notez que cette erreur se produit même si myFcn n’est jamais appelé !

La plupart des collisions de nommage se produisent dans deux cas :
1) Deux définitions (ou plus) d’une fonction (ou d’une variable globale) sont introduites dans des fichiers distincts compilés dans le même programme. Cela entraînera une erreur d’éditeur de liens, comme indiqué ci-dessus.
2) Deux définitions (ou plus) d’une fonction (ou d’une variable globale) sont introduites dans le même fichier (souvent via un #include). Cela entraînera une erreur de compilation.

À mesure que les programmes s’agrandissent et utilisent davantage d’identifiants, les chances d’introduction d’une collision de nommage augmentent considérablement. La bonne nouvelle est que C++ fournit de nombreux mécanismes pour éviter les collisions de noms. La portée locale, qui empêche les variables locales définies à l’intérieur des fonctions d’entrer en conflit les unes avec les autres, est l’un de ces mécanismes. Mais la portée locale ne fonctionne pas pour les noms de fonctions. Alors, comment empêchons-nous les noms de fonctions d’entrer en conflit les uns avec les autres?

Qu’est-ce qu’un espace de noms ?

Revenons un instant à notre analogie d’adresse, avoir deux rues avant n’était problématique que parce que ces rues existaient dans la même ville. D’un autre côté, si vous deviez livrer le courrier à deux adresses, une au 209 Front Street à Mill City et une autre au 417 Front Street à Jonesville, il n’y aurait aucune confusion quant à l’endroit où aller. Autrement dit, les villes fournissent des regroupements qui nous permettent de désambiguer des adresses qui pourraient autrement entrer en conflit les unes avec les autres. Les espaces de noms agissent comme les villes dans cette analogie.

Un espace de noms est une région qui vous permet de déclarer des noms à l’intérieur de celui-ci à des fins de désambiguïsation. L’espace de noms fournit une portée (appelée portée d’espace de noms) aux noms déclarés à l’intérieur de celui-ci – ce qui signifie simplement que tout nom déclaré à l’intérieur de l’espace de noms ne sera pas confondu avec des noms identiques dans d’autres étendues.

Key insight

Un nom déclaré dans un espace de noms ne sera pas confondu avec un nom identique déclaré dans une autre portée.

Dans un espace de noms, tous les noms doivent être uniques, sinon une collision de noms en résultera.

Les espaces de noms sont souvent utilisés pour regrouper les identifiants associés dans un grand projet afin de s’assurer qu’ils ne entrent pas en collision par inadvertance avec d’autres identifiants. Par exemple, si vous placez toutes vos fonctions mathématiques dans un espace de noms appelé math, vos fonctions mathématiques n’entreront pas en collision avec des fonctions nommées de manière identique en dehors de l’espace de noms mathématiques.

Nous parlerons de la façon de créer vos propres espaces de noms dans une prochaine leçon.

L’espace de noms global

En C++, tout nom qui n’est pas défini dans une classe, une fonction ou un espace de noms est considéré comme faisant partie d’un espace de noms implicitement défini appelé espace de noms global (parfois aussi appelé portée globale).

Dans l’exemple en haut de la leçon, les fonctions main() et les deux versions de myFcn() sont définies dans l’espace de noms global. La collision de nommage rencontrée dans l’exemple se produit parce que les deux versions de myFcn() se retrouvent dans l’espace de noms global, ce qui viole la règle selon laquelle tous les noms de l’espace de noms doivent être uniques.

L’espace de noms std

Lorsque C++ a été conçu à l’origine, tous les identifiants de la bibliothèque standard C++ (y compris std::cin et std::cout) étaient disponibles pour être utilisés sans le préfixe std:: (ils faisaient partie de l’espace de noms global). Cependant, cela signifie que tout identifiant de la bibliothèque standard peut potentiellement entrer en conflit avec tout nom que vous avez choisi pour vos propres identifiants (également définis dans l’espace de noms global). Le code qui fonctionnait peut soudainement avoir un conflit de dénomination lorsque vous # incluez un nouveau fichier de la bibliothèque standard. Ou pire, les programmes qui compileraient sous une version de C ++ pourraient ne pas compiler sous une future version de C ++, car les nouveaux identifiants introduits dans la bibliothèque standard pourraient avoir un conflit de dénomination avec du code déjà écrit. C++ a donc déplacé toutes les fonctionnalités de la bibliothèque standard dans un espace de noms nommé « std » (abréviation de standard).

Il s’avère que le nom de std::cout n’est pas vraiment std::cout. C’est en fait juste cout, et std est le nom de l’espace de noms dont l’identifiant cout fait partie. Étant donné que cout est défini dans l’espace de noms std, le nom cout n’entrera pas en conflit avec les objets ou fonctions nommés cout que nous créons dans l’espace de noms global.

De même, lorsque vous accédez à un identifiant défini dans un espace de noms (par exemple std::cout), vous devez indiquer au compilateur que nous recherchons un identifiant défini dans l’espace de noms (std).

Key insight

Lorsque vous utilisez un identifiant défini dans un espace de noms (tel que l’espace de noms std), vous devez indiquer au compilateur que l’identifiant se trouve dans l’espace de noms.

Il existe différentes façons de le faire.

Qualificateur d’espace de noms explicite std::

Le moyen le plus simple de dire au compilateur que nous voulons utiliser cout à partir de l’espace de noms std est d’utiliser explicitement le préfixe std::. Par exemple:

1
2
3
4
5
6
7

# inclure < iostream >
int main()
{
std::cout<< « Bonjour le monde! »; // quand on dit cout, on entend le cout défini dans l’espace de noms std
return 0;
}

Le symbole :: est un opérateur appelé opérateur de résolution de portée. L’identifiant à gauche du symbole :: identifie l’espace de noms dans lequel se trouve le nom à droite du symbole ::. Si aucun identifiant à gauche du symbole :: n’est fourni, l’espace de noms global est supposé.

Donc, quand nous disons std::cout, nous disons « le cout qui vit dans l’espace de noms std ».

C’est le moyen le plus sûr d’utiliser cout, car il n’y a aucune ambiguïté sur le cout auquel nous faisons référence (celui de l’espace de noms std).

Meilleure pratique

Utilisez des préfixes d’espace de noms explicites pour accéder aux identifiants définis dans un espace de noms.

Utiliser l’espace de noms std (et pourquoi l’éviter)

Une autre façon d’accéder aux identifiants dans un espace de noms consiste à utiliser une instruction de directive using. Voici notre programme original « Hello world » avec une directive using:

1
2
3
4
5
6
7
8
9

# include < iostream >
using namespace std; // il s’agit d’une directive using indiquant au compilateur de vérifier l’espace de noms std lors de la résolution d’identifiants sans préfixe
int main()
{
cout < < « Bonjour le monde! »; //cout n’a pas de préfixe, donc le compilateur vérifiera si cout est défini localement ou dans l’espace de noms std
return 0;
}

Une directive using indique au compilateur de vérifier un espace de noms spécifié lorsqu’il tente de résoudre un identifiant qui n’a pas de préfixe d’espace de noms. Ainsi, dans l’exemple ci-dessus, lorsque le compilateur va déterminer quel est l’identifiant cout, il vérifiera à la fois localement (où il n’est pas défini) et dans l’espace de noms std (où il correspondra à std::cout).

De nombreux textes, tutoriels et même certains compilateurs recommandent ou utilisent une directive using en haut du programme. Cependant, utilisé de cette manière, c’est une mauvaise pratique, et fortement déconseillé.

Considérez le programme suivant:

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

# include < iostream > // importe la déclaration de std::cout
en utilisant l’espace de noms std; // rend std::cout accessible en tant que « cout »
int cout() // déclare notre propre fonction « cout
{
retour 5;
}
int principal()
{
cout < < « Bonjour, monde! »; // Erreur de compilation! Quel cout voulons-nous ici? Celui de l’espace de noms std ou celui que nous avons défini ci-dessus?
retour 0;
}

Le programme ci-dessus ne compile pas, car le compilateur ne peut plus dire si nous voulons la fonction cout que nous avons définie ou la cout qui est définie dans l’espace de noms std.

Lors de l’utilisation d’une directive using de cette manière, tout identifiant que nous définissons peut entrer en conflit avec tout identifiant de nom identique dans l’espace de noms std. Pire encore, bien qu’un nom d’identifiant puisse ne pas entrer en conflit aujourd’hui, il peut entrer en conflit avec de nouveaux identifiants ajoutés à l’espace de noms std lors de futures révisions de langage. C’était tout l’intérêt de déplacer tous les identifiants de la bibliothèque standard dans l’espace de noms std en premier lieu!

Avertissement

Évitez d’utiliser des directives (telles que l’utilisation de l’espace de noms std;) en haut de votre programme. Ils violent la raison pour laquelle les espaces de noms ont été ajoutés en premier lieu.

Nous parlerons plus en détail de l’utilisation des instructions (et de la façon de les utiliser de manière responsable) dans la leçon 6.12 – Utilisation des instructions.

2.9 — Introduction au préprocesseur

Index

2.7 — Programmes avec plusieurs fichiers de code

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.