std::string hello = "Hello, world";
std::cout << hello; // Appelle std::operator<<
// grâce à la règle de Koenig }
Grâce à la règle de Koenig, le compilateur prend en compte automatiquement les fonctions de l’espace de nommage où est déclarée string (c’est-à-dire std) pour la résolution de l’appel « std ::cout<<hello ; ».
Sans cette règle, il y aurait la solution d’ajouter une instruction « using std ::operator<< ; » en haut de chaque module concerné, ce qui deviendrait vite fastidieux dans le cas de plusieurs opérateurs ; d’utiliser « using namespace std ; », ce qui aurait pour effet de copier tous les noms de std dans l’espace de nommage par défaut et, par là-même, de supprimer tout l’intérêt de l’utilisation des espaces de nom-mage ; ou bien encore de faire explicitement référence à la fonction
« std::operator<< (std::cout,hello) ; » ce qui obligerait à se priver de la syn-taxe habituelle des opérateurs, pourtant très confortable. Aucune de ces solutions n’est viable, ceci démontre bien à quel point la règle de Koenig est pratique !
En résumé, lorsque vous définissez, dans un même espace de nommage, une classe et une fonction globale faisant référence à cette classe1, le compilateur va en quelque sorte établir une relation entre les deux.2 Ceci nous ramène à la notion d’interface de classe, comme l’exemple de Myers va nous le montrer.
Règle de Koenig, suite et fin : l’exemple de Myers
Considérons l’exemple suivant – légèrement modifié par rapport au 3(a) :
//*** Exemple 4 (a) // Fichier t.h namespace NS {
class T { };
}
// Fichier main.cpp
void f( NS::T );
int main() {
NS::T parm;
f(parm); // OK: calls global f }
1. Par valeur, référence, pointeur ou autre.
2. Certes moins forte que la relation existant entre une classe et une de ses fonctions mem-bres. À ce sujet, voir le paragraphe « ‘Être membre’ ou ‘faire partie’ d’une classe » plus loin dans ce chapitre.
D’un côté, nous avons une classe T définie dans un espace de nommage NS, de l’autre, nous avons un code client utilisant T et définissant, pour ses propres besoins, une fonction globale f() utilisant T. Pour l’instant, rien à signaler...
Que se passe t’il si, un jour, l’auteur de « t.h » décide d’ajouter une fonction f()
dans NS ?
//*** Exemple 4 (b) // Fichier t.h namespace NS {
class T { };
void f( T ); // <-- Nouvelle fonction }
void f( NS::T );
int main() {
NS::T parm;
f(parm); // Appel ambigu : Appelle NS::f } // ou la fonction globale f ?
Le fait d’ajouter une fonction f() à l’intérieur de l’espace de nommage NS a rendu inopérant du code client extérieur à NS, ce qui peut paraître plutôt gênant. Mais attendez, il y a pire :
//*** Exemple de Myers: "Avant"
namespace A {
class X { };
}
namespace B {
void f( A::X );
void g( A::X parm ) {
f(parm); // OK: Appelle B::f() }
}
Pour l’instant, tout va bien. Jusqu’au jour où l’auteur de A ajoute une nouvelle fonction :
//*** Exemple de Myers: "Après"
namespace A {
class X { };
void f( X ); // <-- Nouvelle fonction }
namespace B {
void f( A::X );
void g( A::X parm ) {
f(parm); // Appel ambigu : A::f ou B::f ? }
}
Là encore, le fait d’ajouter une fonction à l’intérieur de A peut rendre inopérant du code extérieur à A. À première vue, ceci peut paraître plutôt gênant et contraire aux principes même des espaces de nommage, dont la fonction est d’isoler différentes par-ties du code. C’est d’ailleurs d’autant plus troublant que le lien entre le code concerné et A n’est pas clairement apparent.
Si on y réfléchit un peu, ceci n’est pas un problème, au contraire ! Il se passe exac-tement ce qui doit se passer1. Il est tout à fait normal que la fonction f(X) de A inter-vienne dans la résolution de l’appel f(parm) – et, en l’occurrence, crée une ambiguïté.
Cette fonction fait référence à X et est fournie avec X, donc, en vertu du principe de l’interface de classe, elle fait partie de la classe X. À la limite, peu importe que f()
soit une fonction globale ou une fonction membre, l’important est qu’elle soit ratta-chée conceptuellement à X.
Voici une autre version de l’exemple de Myers :
//*** Exemple de Myers : "Après" (autre version) namespace A
{
class X { };
ostream& operator<<( ostream&, const X& );
}
namespace B {
ostream& operator<<( ostream&, const A::X& );
void g( A::X parm ) {
cout << parm; // Appel ambigu : A::operator<<
} // ou B::operator<< ? }
Une fois encore, il est normal que l’appel cout<<parm soit ambigu. L’auteur de la fonction g() doit explicitement indiquer quelle fonction operator<< il souhaite appe-ler, celle de B – qu’il est normal de prendre en compte car elle est située dans le même espace de nommage – ou celle de A – qu’il est également normal de prendre en compte car elle « fait partie » de X, au nom du principe d’interface de classe :
L’interface d’une classe X est constituée de toutes les fonctions, membres ou globales, qui « font référence à X » et « sont fournies avec X ».
L’exemple de Myers a le mérite de montrer que les espaces de nommage ne sont pas aussi indépendants qu’on le croit mais également que cette dépendance est saine 1. C’est cet exemple, cité à la réunion du Comité de Normalisation C++ en novembre 1997
à Morristown, qui m’a amené à réfléchir sur le sujet des dépendances en général.
et voulue. : ce n’est pas un hasard que la notion d’interface de classe soit cohérente avec la règle de Koenig. Cette règle a justement été établie en vertu de cette notion d’interface de classe.
Pour être complet sur le sujet, nous allons maintenant voir en quoi « être membre » d’une classe introduit une relation plus forte qu’être une fonction globale « faisant partie de la classe ».
« Être membre » ou « faire partie » d’une classe
Le principe d’interface de classe énonce que des fonctions membres et non-mem-bres peuvent toutes deux faire conceptuellement « partie » d’une classe. Il n’en conclut pas pour autant que membres et non-membres sont équivalents. Il est clair qu’une fonction membre est liée de manière plus forte à une classe car elle a accès à l’ensemble des membres privés alors que ce n’est pas le cas pour une fonction globale – à moins qu’elle ne soit déclarée comme friend. De plus, lors d’une résolution de nom – faisant appel ou non à la règle de Koenig, une fonction membre prend toujours le pas sur une fonction globale :
// Ceci n’est PAS l’exemple de Myers namespace A
{
class X {} ; void f(X) ; }
class B // ‘class’, pas ‘namespace’
{
void f(A::X);
void g(A::X parm) {
f(parm) ; // Appelle B::f(), pas d’ambiguïté }
};
Dans cet exemple, la classe B contenant une fonction membre f(A::X), le compi-lateur fait appeler cette fonction, ne cherchant même pas à regarder ce qui est dans A, bien que f prenne un paramètre dont le type est déclaré dans A.
En résumé, une fonction membre est toujours plus fortement liée à une classe qu’une fonction non-membre – même faisant « partie » de la classe au nom de la notion d’interface de classe – notamment pour ce qui concerne l’accès aux membres privés et la résolution des noms.
Il y a en général deux manières courantes d’implémenter l’opérateur << : soit comme une fonction globale utilisant l’interface publique de classe (ou à la limite, l’interface non-publique, si elle est déclarée comme friend) ou comme une fonction globale appelant une fonction membre virtuelle Print().
Quelle est la meilleure méthode ? Précisez les avantages et inconvénients de chacune.
Une des conséquences pratiques de la notion d’interface de classe est qu’elle per-met d’identifier clairement les dépendances entre une classe et ses « clients ».
Nous allons voir ici l ’exemple concret de la fonction operator<<.