• 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 :

    using namespace nom;
    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

    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

    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

    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 ;
    }

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    9.5. Exceptions dans les constructeurs

    Il est parfaitement légal de lancer une exception dans un constructeur. En fait, c'est même la seule solution pour signaler une erreur lors de la construction d'un objet, puisque les constructeurs n'ont pas de valeur de retour.

    Lorsqu'une exception est lancée à partir d'un constructeur, la construction de l'objet échoue. Par conséquent, le compilateur n'appellera jamais le destructeur pour cet objet, puisque cela n'a pas de sens. Cependant, ce comportement soulève le problème des objets partiellement initialisés, pour lesquels il est nécessaire de faire un peu de nettoyage à la suite du lancement de l'exception. Le C++ dispose donc d'une syntaxe particulière pour les constructeurs des objets susceptibles de lancer des exceptions. Cette syntaxe permet simplement d'utiliser un bloc try pour le corps de fonction des constructeurs. Les blocs catch suivent alors la définition du constructeur, et effectuent la libération des ressources que le constructeur aurait pu allouer avant que l'exception ne se produise.

    Le comportement du bloc catch des constructeurs avec bloc try est différent de celui des blocs catch classiques. En effet, les exceptions ne sont normalement pas relancées une fois qu'elles ont été traitées. Comme on l'a vu ci-dessus, il faut utiliser explicitement le mot clé throw pour relancer une exception à l'issue de son traitement. Dans le cas des constructeurs avec un bloc try cependant, l'exception est systématiquement relancée. Le bloc catch du constructeur ne doit donc prendre en charge que la destruction des données membres partiellement construites, et il faut toujours capter l'exception au niveau du programme qui a cherché à créer l'objet.

    Note : Cette dernière règle implique que les programmes déclarant des objets globaux dont le constructeur peut lancer une exception risquent de se terminer en catastrophe. En effet, si une exception est lancée par ce constructeur à l'initialisation du programme, aucun gestionnaire d'exception ne sera en mesure de la capter lorsque le bloc catch la relancera.

    De même, lorsque la construction de l'objet se fait dans le cadre d'une allocation dynamique de mémoire, le compilateur appelle automatiquement l'opérateur delete afin de restituer la mémoire allouée pour cet objet. Il est donc inutile de restituer la mémoire de l'objet alloué dans le traitement de l'exception qui suit la création dynamique de l'objet, et il ne faut pas y appeler l'opérateur delete manuellement.

    Note : Comme il l'a été dit plus haut, le compilateur n'appelle pas le destructeur pour les objets dont le constructeur a généré une exception. Cette règle est valide même dans le cas des objets alloués dynamiquement. Le comportement de l'opérateur delete est donc lui aussi légèrement modifié par le fait que l'exception s'est produite dans un constructeur.

    Exemple 9-5. Exceptions dans les constructeurs

    #include <iostream>
    #include <stdlib.h>
    
    using namespace std;
    
    class A
    {
        char *pBuffer;
        int  *pData;
    
    public:
        A() throw (int);
    
        ~A()
        {
            cout << "A::~A()" << endl;
        }
    
        static void *operator new(size_t taille)
        {
            cout << "new()" << endl;
            return malloc(taille);
        }
    
        static void operator delete(void *p)
        {
            cout << "delete" << endl;
            free(p);
        }
    };
    
    // Constructeur susceptible de lancer une exception :
    A::A() throw (int)
    try
    {
        pBuffer = NULL;
        pData = NULL;
        cout << "Début du constructeur" << endl;
        pBuffer = new char[256];
        cout << "Lancement de l'exception" << endl;
        throw 2;
        // Code inaccessible :
        pData = new int;
    }
    catch (int)
    {
        cout << "Je fais le ménage..." << endl;
        delete[] pBuffer;
        delete pData;
    }
    
    
    int main(void)
    {
        try
        {
            A *a = new A;
        }
        catch (...)
        {
            cout << "Aïe, même pas mal !" << endl;
        }
        return 0;
    }

    Dans cet exemple, lors de la création dynamique d'un objet A, une erreur d'initialisation se produit et une exception est lancée. Celle-ci est alors traitée dans le bloc catch qui suit la définition du constructeur de la classe A. L'opérateur delete est bien appelé automatiquement, mais le destructeur de A n'est jamais exécuté.

    En général, si une classe hérite de une ou plusieurs classes de base, l'appel aux constructeurs des classes de base doit se faire entre le mot clé try et la première accolade. En effet, les constructeurs des classes de base sont susceptibles, eux aussi, de lancer des exceptions. La syntaxe est alors la suivante :

    Classe::Classe
       try : Base(paramètres) [, Base(paramètres) [...]]
    {
    }
    catch ...

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    9.4. Hiérarchie des exceptions

    Le mécanisme des exceptions du C++ se base sur le typage des objets, puisque le lancement d'une exception nécessite la construction d'un objet qui la caractérise, et le bloc catch destination de cette exception sera sélectionné en fonction du type de cet objet. Bien entendu, les objets utilisés pour lancer les exceptions peuvent contenir des informations concernant la nature des erreurs qui se produisent, mais il est également possible de classifier ces erreurs par catégories en se basant sur leurs types.

    En effet, les objets exceptions peuvent être des instances de classes disposant de relations d'héritage. Comme les objets des classes dérivées peuvent être considérés comme des instances de leurs classes de base, les gestionnaires d'exception peuvent récupérer les exceptions de ces classes dérivées en récupérant un objet du type d'une de leurs classes de base. Ainsi, il est possible de classifier les différents cas d'erreurs en définissant une hiérarchie de classe d'exceptions, et d'écrire des traitements génériques en n'utilisant que les objets d'un certain niveau dans cette hiérarchie.

    Le mécanisme des exceptions se montre donc plus puissant que toutes les autres méthodes de traitement d'erreurs à ce niveau, puisque la sélection du gestionnaire d'erreur est automatiquement réalisée par le langage. Cela peut être très pratique pour peu que l'on ait défini correctement sa hiérarchie de classes d'exceptions.

    Exemple 9-4. Classification des exceptions

    #include <iostream>
    
    using namespace std;
    
    // Classe de base de toutes les exceptions :
    class ExRuntimeError
    {
    };
    
    // Classe de base des exceptions pouvant se produire
    // lors de manipulations de fichiers :
    class ExFileError : public ExRuntimeError
    {
    };
    
    // Classes des erreurs de manipulation des fichiers :
    class ExInvalidName : public ExFileError
    {
    };
    
    class ExEndOfFile : public ExFileError
    {
    };
    
    class ExNoSpace : public ExFileError
    {
    };
    
    class ExMediumFull : public ExNoSpace
    {
    };
    
    class ExFileSizeMaxLimit : public ExNoSpace
    {
    };
    
    // Fonction faisant un travail quelconque sur un fichier :
    void WriteData(const char *szFileName)
    {
        // Exemple d'erreur :
        if (szFileName == NULL) throw ExInvalidName();
        else
        {
            // Traitement de la fonction
            // etc.
    
            // Lancement d'une exception :
            throw ExMediumFull();
        }
    }
    
    void Save(const char *szFileName)
    {
        try
        {
            WriteData(szFileName);
        }
        // Traitement d'un erreur spécifique :
        catch (ExInvalidName &)
        {
            cout << "Impossible de faire la sauvegarde" << endl;
        }
        // Traitement de toutes les autres erreurs en groupe :
        catch (ExFileError &)
        {
            cout << "Erreur d'entrée / sortie" << endl;
        }
    }
    
    int main(void)
    {
        Save(NULL);
        Save("data.dat");
        return 0;
    }

    La bibliothèque standard C++ définit elle-même un certain nombre d'exceptions standards, qui sont utilisées pour signaler les erreurs qui se produisent à l'exécution des programmes. Quelques-unes de ces exceptions ont déjà été présentées avec les fonctionnalités qui sont susceptibles de les lancer. Vous trouverez une liste complète des exceptions de la bibliothèque standard du C++ dans la section 13.2.


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    9.3. Liste des exceptions autorisées pour une fonction

    Il est possible de spécifier les exceptions qui peuvent être lancées par une fonction. Pour cela, il faut faire suivre son en-tête du mot clé throw avec, entre parenthèses et séparées par des virgules, les classes des exceptions qu'elle est autorisée à lancer. Par exemple, la fonction suivante :

    int fonction_sensible(void)
    throw (int, double, erreur)
    {
        ...
    }
    n'a le droit de lancer que des exceptions du type int, double ou erreur. Si une exception d'un autre type est lancée, par exemple une exception du type char *, il se produit encore une fois une erreur à l'exécution.

    En fait, la fonction std::unexpected est appelée. Cette fonction se comporte de manière similaire à std::terminate, puisqu'elle appelle par défaut une fonction de traitement de l'erreur qui elle-même appelle la fonction std::terminate (et donc abort en fin de compte). Cela conduit à la terminaison du programme. On peut encore une fois changer ce comportement par défaut en remplaçant la fonction appelée par std::unexpected par une autre fonction à l'aide de std::set_unexpected, qui est déclarée dans le fichier d'en-tête exception. Cette dernière attend en paramètre un pointeur sur la fonction de traitement d'erreur, qui ne doit prendre aucun paramètre et qui ne doit rien renvoyer. std::set_unexpected renvoie le pointeur sur la fonction de traitement d'erreur précédemment appelée par std::unexpected.

    Note : Comme leurs noms l'indiquent, std::unexpected et std::set_unexpected sont déclarées dans l'espace de nommage std::, qui est réservé pour les objets de la bibliothèque standard C++. Si vous ne voulez pas avoir à utiliser systématiquement le préfixe std:: pour ces noms, vous devrez ajouter la ligne « using namespace std; » après avoir inclus l'en-tête exception. Vous obtiendrez de plus amples renseignements sur les espaces de nommage dans le chapitre 11.

    Il est possible de relancer une autre exception à l'intérieur de la fonction de traitement d'erreur. Si cette exception satisfait la liste des exceptions autorisées, le programme reprend son cours normalement dans le gestionnaire correspondant. C'est généralement ce que l'on cherche à faire. Le gestionnaire peut également lancer une exception de type std::bad_exception, déclarée comme suit dans le fichier d'en-tête exception :

    class bad_exception : public exception
    {
    public:
        bad_exception(void) throw();
        bad_exception(const bad_exception &) throw();
        bad_exception &operator=(const bad_exception &) throw();
        virtual ~bad_exception(void) throw();
        virtual const char *what(void) const throw();
    };
    Cela a pour conséquence de terminer le programme.

    Enfin, le gestionnaire d'exceptions non autorisées peut directement mettre fin à l'exécution du programme en appelant std::terminate. C'est le comportement utilisé par la fonction std::unexpected définie par défaut.

    Exemple 9-3. Gestion de la liste des exceptions autorisées

    #include <iostream>
    #include <exception>
    
    using namespace std;
    
    void mon_gestionnaire(void)
    {
        cout << "Une exception illégale a été lancée." << endl;
        cout << "Je relance une exception de type int." << endl;
        throw 2;
    }
    
    int f(void) throw (int)
    {
        throw "5.35";
    }
    
    int main(void)
    {
        set_unexpected(&mon_gestionnaire);
        try
        {
            f();
        }
        catch (int i)
        {
            cout << "Exception de type int reçue : " <<
               i << endl;
        }
        return 0;
    }

    Note : La liste des exceptions autorisées dans une fonction ne fait pas partie de sa signature. Elle n'intervient donc pas dans les mécanismes de surcharge des fonctions. De plus, elle doit se placer après le mot clé const dans les déclarations de fonctions membres const (en revanche, elle doit se placer avant =0 dans les déclarations des fonctions virtuelles pures).

    On prendra garde au fait que les exceptions ne sont pas générées par le mécanisme de gestion des erreurs du C++ (ni du C). Cela signifie que pour avoir une exception, il faut la lancer, le compilateur ne fera pas les tests pour vous (tests de débordements numériques dans les calculs par exemple). Cela supposerait de prédéfinir un ensemble de classes pour les erreurs génériques. Les tests de validité d'une opération doivent donc être faits malgré tout et, le cas échéant, il faut lancer une exception pour reporter le traitement en cas d'échec. De même, les exceptions générées par la machine hôte du programme ne sont en général pas récupérées par les implémentations et, si elles le sont, les programmes qui les utilisent ne sont pas portables.


    votre commentaire
  • 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 :

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

    Exemple 11-7. Déclaration using

    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

    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

    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

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

    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

    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.


    votre commentaire