Cours de C/C++


précédentsommairesuivant

11. Les espaces de nommage

Les espaces de nommage sont des zones de déclaration qui permettent de délimiter la recherche des noms des identificateurs par le compilateur. Leur but est essentiellement de regrouper les identificateurs logiquement et d'éviter les conflits de noms entre plusieurs parties d'un même projet. Par exemple, si deux programmeurs définissent différemment une même structure dans deux fichiers différents, un conflit entre ces deux structures aura lieu au mieux à l'édition de liens, et au pire lors de l'utilisation commune des sources de ces deux programmeurs. Ce type de conflit provient du fait que le C++ ne fournit qu'un seul espace de nommage de portée globale, dans lequel il ne doit y avoir aucun conflit de nom. Grâce aux espaces de nommage non globaux, ce type de problème peut être plus facilement évité, parce que l'on peut éviter de définir les objets globaux dans la portée globale.

11.1. Définition des espaces de nommage

11.1.1. Espaces de nommage nommées

Lorsque le programmeur donne un nom à un espace de nommage, celui-ci est appelé un espace de nommage nommé. La syntaxe de ce type d'espace de nommage est la suivante :

 
Sélectionnez

namespace nom
{
    déclarations | définitions
}
 

nom est le nom de l'espace de nommage, et déclarations et définitions sont les déclarations et les définitions des identificateurs qui lui appartiennent.

Contrairement aux régions déclaratives classiques du langage (comme par exemple les classes), un namespace peut être découpé en plusieurs morceaux. Le premier morceaux sert de déclaration, et les suivants d'extensions. La syntaxe pour une extension d'espace de nommage est exactement la même que celle de la partie de déclaration.

Exemple 11-1. Extension de namespace
Sélectionnez

namespace A   // Déclaration de l'espace de nommage A.
{
   int i;
}
 
namespace B   // Déclaration de l'espace de nommage B.
{
    int i;
}
 
namespace A   // Extension de l'espace de nommage A.
{
    int j;
}
 

Les identificateurs déclarés ou définis à l'intérieur d'un même espace de nommage ne doivent pas entrer en conflit. Ils peuvent avoir les mêmes noms, mais seulement dans le cadre de la surcharge. Un espace de nommage se comporte donc exactement comme les zones de déclaration des classes et de la portée globale.

L'accès aux identificateurs des espaces de nommage se fait par défaut grâce à l'opérateur de résolution de portée (::), et en qualifiant le nom de l'identificateur à utiliser du nom de son espace de nommage. Cependant, cette qualification est inutile à l'intérieur de l'espace de nommage lui-même, exactement comme pour les membres des classes à l'intérieur de leur classe.

Exemple 11-2. Accès aux membres d'un namespace
Sélectionnez

int i=1;    // i est global.
 
namespace A
{
    int i=2; // i de l'espace de nommage A.
    int j=i; // Utilise A::i.
}
 
int main(void)
{
    i=1;     // Utilise ::i.
    A::i=3;  // Utilise A::i.
    return 0;
}
 

Les fonctions membres d'un espace de nommage peuvent être définies à l'intérieur de cet espace, exactement comme les fonctions membres de classes. Elles peuvent également être définies en dehors de cet espace, si l'on utilise l'opérateur de résolution de portée. Les fonctions ainsi définies doivent apparaître après leur déclaration dans l'espace de nommage.

Exemple 11-3. Définition externe d'une fonction de namespace
Sélectionnez

namespace A
{
    int f(void);   // Déclaration de A::f.
}
 
int A::f(void)     // Définition de A::f.
{
    return 0;
}
 

Il est possible de définir un espace de nommage à l'intérieur d'un autre espace de nommage. Cependant, cette déclaration doit obligatoirement avoir lieu au niveau déclaratif le plus externe de l'espace de nommage qui contient le sous-espace de nommage. On ne peut donc pas déclarer d'espaces de nommage à l'intérieur d'une fonction ou à l'intérieur d'une classe.

Exemple 11-4. Définition de namespace dans un namespace
Sélectionnez

namespace Conteneur
{
    int i;              // Conteneur::i.
    namespace Contenu
    {
        int j;          // Conteneur::Contenu::j.
    }
}
 

11.1.2. Espaces de nommage anonymes

Lorsque, lors de la déclaration d'un espace de nommage, aucun nom n'est donné, un espace de nommage anonyme est créé. Ce type d'espace de nommage permet d'assurer l'unicité du nom de l'espace de nommage ainsi déclaré. Les espaces de nommage anonymes peuvent donc remplacer efficacement le mot clé static pour rendre unique des identificateurs dans un fichier. Cependant, elles sont plus puissantes, parce que l'on peut également déclarer des espaces de nommage anonymes à l'intérieur d'autres espaces de nommage.

Exemple 11-5. Définition de namespace anonyme
Sélectionnez

namespace
{
    int i;         // Équivalent à unique::i;
}
 

Dans l'exemple précédent, la déclaration de i se fait dans un espace de nommage dont le nom est choisi par le compilateur de manière unique. Cependant, comme on ne connaît pas ce nom, le compilateur utilise une directive using (voir plus loin) afin de pouvoir utiliser les identificateurs de cet espace de nommage anonyme sans préciser leur nom complet avec l'opérateur de résolution de portée.

Si, dans un espace de nommage, un identificateur est déclaré avec le même nom qu'un autre identificateur déclaré dans un espace de nommage plus global, l'identificateur global est masqué. De plus, l'identificateur ainsi défini ne peut être accédé en dehors de son espace de nommage que par un nom complètement qualifié à l'aide de l'opérateur de résolution de portée. Toutefois, si l'espace de nommage dans lequel il est défini est un espace de nommage anonyme, cet identificateur ne pourra pas être référencé, puisqu'on ne peut pas préciser le nom des espaces de nommage anonymes.

Exemple 11-6. Ambiguïtés entre namespaces
Sélectionnez

namespace
{
    int i;          // Déclare unique::i.
}
 
void f(void)
{
    ++i;            // Utilise unique::i.
}
 
namespace A
{
    namespace
    {
        int i;     // Définit A::unique::i.
        int j;     // Définit A::unique::j.
    }
 
    void g(void)
    {
        ++i;       // Erreur : ambiguïté entre unique::i
                   // et A::unique::i.
        ++A::i;    // Erreur : A::i n'est pas défini
                   // (seul A::unique::i l'est).
        ++j;       // Correct : ++A::unique::j.
    }
}
 

11.1.3. Alias d'espaces de nommage

Lorsqu'un espace de nommage porte un nom très compliqué, il peut être avantageux de définir un alias pour ce nom. L'alias aura alors un nom plus simple.

Cette opération peut être réalisée à l'aide de la syntaxe suivante :

namespace nom_alias = nom;

nom_alias est ici le nom de l'alias de l'espace de nommage, et nom est le nom de l'espace de nommage lui-même.

Les noms donnés aux alias d'espaces de nommage ne doivent pas entrer en conflit avec les noms des autres identificateurs du même espace de nommage, que celui-ci soit l'espace de nommage de portée globale ou non.

11.2. Déclaration using

Les déclarations using permettent d'utiliser un identificateur d'un espace de nommage de manière simplifiée, sans avoir à spécifier son nom complet (c'est-à-dire le nom de l'espace de nommage suivi du nom de l'identificateur).

11.2.1. Syntaxe des déclarations using

La syntaxe des déclarations using est la suivante :

 
Sélectionnez

using identificateur;
 

où identificateur est le nom complet de l'identificateur à utiliser, avec qualification d'espace de nommage.

Exemple 11-7. Déclaration using
Sélectionnez

namespace A
{
    int i;        // Déclare A::i.
    int j;        // Déclare A::j.
}
 
void f(void)
{
    using A::i;   // A::i peut être utilisé sous le nom i.
    i=1;          // Équivalent à A::i=1.
    j=1;          // Erreur ! j n'est pas défini !
    return ;
}
 

Les déclarations using permettent en fait de déclarer des alias des identificateurs. Ces alias doivent être considérés exactement comme des déclarations normales. Cela signifie qu'ils ne peuvent être déclarés plusieurs fois que lorsque les déclarations multiples sont autorisées (déclarations de variables ou de fonctions en dehors des classes), et de plus ils appartiennent à l'espace de nommage dans lequel ils sont définis.

Exemple 11-8. Déclarations using multiples
Sélectionnez

namespace A
{
    int i;
    void f(void)
    {
    }
}
 
namespace B
{
    using A::i;  // Déclaration de l'alias B::i, qui représente A::i.
    using A::i;  // Légal : double déclaration de A::i.
 
    using A::f;  // Déclare void B::f(void),
                 // fonction identique à A::f.
}
 
int main(void)
{
    B::f();      // Appelle A::f.
    return 0;
}
 

L'alias créé par une déclaration using permet de référencer uniquement les identificateurs qui sont visibles au moment où la déclaration using est faite. Si l'espace de nommage concerné par la déclaration using est étendu après cette dernière, les nouveaux identificateurs de même nom que celui de l'alias ne seront pas pris en compte.

Exemple 11-9. Extension de namespace après une déclaration using
Sélectionnez

namespace A
{
    void f(int);
}
 
using A::f;            // f est synonyme de A::f(int).
 
namespace A
{
    void f(char);      // f est toujours synonyme de A::f(int),
                       // mais pas de A::f(char).
}
 
void g()
{
    f('a');            // Appelle A::f(int), même si A::f(char)
                       // existe.
}
 

Si plusieurs déclarations locales et using déclarent des identificateurs de même nom, ou bien ces identificateurs doivent tous se rapporter au même objet, ou bien ils doivent représenter des fonctions ayant des signatures différentes (les fonctions déclarées sont donc surchargées). Dans le cas contraire, des ambiguïtés peuvent apparaître et le compilateur signale une erreur lors de la déclaration using.

Exemple 11-10. Conflit entre déclarations using et identificateurs locaux
Sélectionnez

namespace A
{
    int i;
    void f(int);
}
 
void g(void)
{
    int i;        // Déclaration locale de i.
    using A::i;   // Erreur : i est déjà déclaré.
    void f(char); // Déclaration locale de f(char).
    using A::f;   // Pas d'erreur, il y a surcharge de f.
    return ;
}
 

Note : Ce comportement diffère de celui des directives using. En effet, les directives using reportent la détection des erreurs à la première utilisation des identificateurs ambigus.

11.2.2. Utilisation des déclarations using dans les classes

Une déclaration using peut être utilisée dans la définition d'une classe. Dans ce cas, elle doit se rapporter à une classe de base de la classe dans laquelle elle est utilisée. De plus, l'identificateur donné à la déclaration using doit être accessible dans la classe de base (c'est-à-dire de type protected ou public).

Exemple 11-11. Déclaration using dans une classe
Sélectionnez

namespace A
{
    float f;
}
 
class Base
{
    int i;
public:
    int j;
};
 
class Derivee : public Base
{
    using A::f;      // Illégal : f n'est pas dans une classe
                     // de base.
    using Base::i;   // Interdit : Derivee n'a pas le droit
                     // d'utiliser Base::i.
public:
    using Base::j;   // Légal.
};

Dans l'exemple précédent, seule la troisième déclaration est valide, parce que c'est la seule qui se réfère à un membre accessible de la classe de base. Le membre j déclaré sera donc un synonyme de Base::j dans la classe Derivee.

En général, les membres des classes de base sont accessibles directement. Quelle est donc l'utilité des déclarations using dans les classes ? En fait, elles peuvent être utilisées pour rétablir les droits d'accès, modifiés par un héritage, à des membres de classes de base. Pour cela, il suffit de placer la déclaration using dans une zone de déclaration du même type que celle dans laquelle le membre se trouvait dans la classe de base. Cependant, comme on l'a vu ci-dessus, une classe ne peut pas rétablir les droits d'accès d'un membre de classe de base déclaré en zone private.

Exemple 11-12. Rétablissement de droits d'accès à l'aide d'une directive using
Sélectionnez

class Base
{
public:
    int i;
    int j;
};
 
class Derivee : private Base
{
public:
    using Base::i;  // Rétablit l'accessibilité sur Base::i.
protected:
    using Base::i;  // Interdit : restreint l'accessibilité
                    // sur Base::i autrement que par héritage.
};
 

Note : Certains compilateurs interprètent différemment le paragraphe 11.3 de la norme C++, qui concerne l'accessibilité des membres introduits avec une déclaration using. Selon eux, les déclarations using permettent de restreindre l'accessibilité des droits et non pas de les rétablir. Cela implique qu'il est impossible de redonner l'accessibilité à des données pour lesquelles l'héritage a restreint l'accès. Par conséquent, l'héritage doit être fait de la manière la plus permissive possible, et les accès doivent être ajustés au cas par cas. Bien que cette interprétation soit tout à fait valable, l'exemple donné dans la norme C++ semble indiquer qu'elle n'est pas correcte.

Quand une fonction d'une classe de base est introduite dans une classe dérivée à l'aide d'une déclaration using, et qu'une fonction de même nom et de même signature est définie dans la classe dérivée, cette dernière fonction surcharge la fonction de la classe de base. Il n'y a pas d'ambiguïté dans ce cas.

11.3. Directive using

La directive using permet d'utiliser, sans spécification d'espace de nommage, non pas un identificateur comme dans le cas de la déclaration using, mais tous les identificateurs de cet espace de nommage.

La syntaxe de la directive using est la suivante :

 
Sélectionnez

using namespace nom;
 

où nom est le nom de l'espace de nommage dont les identificateurs doivent être utilisés sans qualification complète.

Exemple 11-13. Directive using
Sélectionnez

namespace A
{
    int i;        // Déclare A::i.
    int j;        // Déclare A::j.
}
 
void f(void)
{
    using namespace A; // On utilise les identificateurs de A.
    i=1;          // Équivalent à A::i=1.
    j=1;          // Équivalent à A::j=1.
    return ;
}
 

Après une directive using, il est toujours possible d'utiliser les noms complets des identificateurs de l'espace de nommage, mais ce n'est plus nécessaire. Les directives using sont valides à partir de la ligne où elles sont déclarées jusqu'à la fin du bloc de portée courante. Si un espace de nommage est étendu après une directive using, les identificateurs définis dans l'extension de l'espace de nommage peuvent être utilisés exactement comme les identificateurs définis avant la directive using (c'est-à-dire sans qualification complète de leurs noms).

Exemple 11-14. Extension de namespace après une directive using
Sélectionnez

namespace A
{
    int i;
}
 
using namespace A;
 
namespace A
{
    int j;
}
 
void f(void)
{
    i=0;    // Initialise A::i.
    j=0;    // Initialise A::j.
    return ;
}
 

Il se peut que lors de l'introduction des identificateurs d'un espace de nommage par une directive using, des conflits de noms apparaissent. Dans ce cas, aucune erreur n'est signalée lors de la directive using. En revanche, une erreur se produit si un des identificateurs pour lesquels il y a conflit est utilisé.

Exemple 11-15. Conflit entre directive using et identificateurs locaux
Sélectionnez

namespace A
{
    int i;  // Définit A::i.
}
 
namespace B
{
    int i;  // Définit B::i.
    using namespace A;   // A::i et B::i sont en conflit.
                         // Cependant, aucune erreur n'apparaît.
}
 
void f(void)
{
    using namespace B;
    i=2;    // Erreur : il y a ambiguïté.
    return ;
}
 

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 developpez.com Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.