• Aucun résultat trouvé

Règle de Koenig, suite et fin : l’exemple de Myers

Dans le document Mieux programmer c++ (Page 148-152)

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<<.

Dans le document Mieux programmer c++ (Page 148-152)