• Chapitre 17. Les conteneurs


    La plupart des programmes informatiques doivent, à un moment donné ou à un autre, conserver un nombre arbitraire de données en mémoire, généralement pour y accéder ultérieurement et leur appliquer des traitements spécifiques. En général, les structures de données utilisées sont toujours manipulées par des algorithmes classiques, que l'on retrouve donc souvent, si ce n'est plusieurs fois, dans chaque programme. Ces structures de données sont communément appelées des conteneurs en raison de leur capacité à contenir d'autres objets.

    Afin d'éviter aux programmeurs de réinventer systématiquement la roue et de reprogrammer les structures de données et leurs algorithmes associés les plus classiques, la bibliothèque standard définit un certain nombre de classes template pour les conteneurs les plus courants. Ces classes sont paramétrées par le type des données des conteneurs et peuvent donc être utilisées virtuellement pour toutes les situations qui se présentent.

    Les conteneurs de la bibliothèque standard ne sont pas définis par les algorithmes qu'ils utilisent, mais plutôt par l'interface qui peut être utilisée par les programmes clients. La bibliothèque standard impose également des contraintes de performances sur ces interfaces en termes de complexité. En réalité, ces contraintes sont tout simplement les plus fortes qui soient, ce qui garantit aux programmes qui les utilisent qu'ils auront les meilleures performances possibles.

    La bibliothèque classifie les conteneurs en deux grandes catégories selon leurs fonctionnalités : les séquences et les conteneurs associatifs. Une séquence est un conteneur capable de stocker ses éléments de manière séquentielle, les uns à la suite des autres. Les éléments sont donc parfaitement identifiés par leur position dans la séquence, et leur ordre relatif est donc important. Les conteneurs associatifs, en revanche, manipulent leurs données au moyen de valeurs qui les identifient indirectement. Ces identifiants sont appelées des clefs par analogie avec la terminologie utilisée dans les bases de données. L'ordre relatif des éléments dans le conteneur est laissé dans ce cas à la libre discrétion de ce dernier et leur recherche se fait donc, généralement, par l'intermédiaire de leurs clefs.

    La bibliothèque fournit plusieurs conteneurs de chaque type. Chacun a ses avantages et ses inconvénients. Comme il n'existe pas de structure de données parfaite qui permette d'obtenir les meilleures performances sur l'ensemble des opérations réalisables, l'utilisateur des conteneurs de la bibliothèque standard devra effectuer son choix en fonction de l'utilisation qu'il désire en faire. Par exemple, certains conteneurs sont plus adaptés à la recherche d'éléments mais sont relativement coûteux pour les opérations d'insertion ou de suppression, alors que pour d'autres conteneurs, c'est exactement l'inverse. Le choix des conteneurs à utiliser sera donc déterminant quant aux performances finales des programmes.

    17.1. Fonctionnalités générales des conteneurs

    Au niveau de leurs interfaces, tous les conteneurs de la bibliothèque standard présentent des similitudes. Cet état de fait n'est pas dû au hasard, mais bel et bien à la volonté de simplifier la vie des programmeurs en évitant de définir une multitude de méthodes ayant la même signification pour chaque conteneur. Cependant, malgré cette volonté d'uniformisation, il existe des différences entre les différents types de conteneurs (séquences ou conteneurs associatifs). Ces différences proviennent essentiellement de la présence d'une clef dans ces derniers, qui permet de manipuler les objets contenus plus facilement.

    Quelle que soit leur nature, les conteneurs fournissent un certain nombre de services de base que le programmeur peut utiliser. Ces services comprennent la définition des itérateurs, de quelques types complémentaires, des opérateurs et de fonctions standards. Les sections suivantes vous présentent ces fonctionnalités générales. Toutefois, les descriptions données ici ne seront pas détaillées outre mesure car elles seront reprises en détail dans la description de chaque conteneur.

    17.1.1. Définition des itérateurs

    Pour commencer, il va de soi que tous les conteneurs de la bibliothèque standard disposent d'itérateurs. Comme on l'a vu dans la section 13.2, les itérateurs constituent une abstraction de la notion de pointeur pour les tableaux. Ils permettent donc de parcourir tous les éléments d'un conteneur séquentiellement à l'aide de l'opérateur de déréférencement * et de l'opérateur d'incrémentation ++.

    Les conteneurs définissent donc tous un type iterator et un type const_iterator, qui sont les types des itérateurs sur les éléments du conteneur. Le type d'itérateur const_iterator est défini pour accéder aux éléments d'un conteneur en les considérant comme des constantes. Ainsi, si le type des éléments stockés dans le conteneur est T, le déréférencement d'un const_iterator renverra un objet de type const T.

    Les conteneurs définissent également les types de données difference_type et size_type que l'on peut utiliser pour effectuer des calculs d'arithmétique des pointeurs avec leurs itérateurs. Le type difference_type se distingue du type size_type par le fait qu'il peut contenir toute valeur issue de la différence entre deux itérateurs, et accepte donc les valeurs négatives. Le type size_type quant à lui est utilisé plus spécialement pour compter un nombre d'éléments, et ne peut prendre que des valeurs positives.

    Afin de permettre l'initialisation de leurs itérateurs, les conteneurs fournissent deux méthodes begin et end, qui renvoient respectivement un itérateur référençant le premier élément du conteneur et la valeur de fin de l'itérateur, lorsqu'il a passé le dernier élément du conteneur. Ainsi, le parcours d'un conteneur se fait typiquement de la manière suivante :

    // Obtient un itérateur sur le premier élément :
    Conteneur::iterateur i = instance.begin();
    // Boucle sur toutes les valeurs de l'itérateur
    // jusqu'à la dernière :
    while (i != instance.end())
    {
    // Travaille sur l'élément référencé par i :
    f(*i);
    // Passe à l'élément suivant :
    ++i;
    }
    Conteneur est la classe de du conteneur et instance en est une instance.

    Note : Pour des raisons de performances et de portabilité, la bibliothèque standard ne fournit absolument aucun support du multithreading sur ses structures de données. En fait, la gestion du multithreading est laissée à la discrétion de chaque implémentation. Généralement, seul le code généré par le compilateur est sûr vis-à-vis des threads (en particulier, les opérateurs d'allocation mémoire new et new[], ainsi que les opérateurs delete et delete[] peuvent être appelés simultanément par plusieurs threads pour des objets différents). Il n'en est pas de même pour les implémentations des conteneurs et des algorithmes de la bibliothèque standard.

    Par conséquent, si vous voulez accéder à un conteneur à partir de plusieurs threads, vous devez prendre en charge vous-même la gestion des sections critiques afin de vous assurer que ce conteneur sera toujours dans un état cohérent. En fait, il est recommandé de le faire même si l'implémentation de la bibliothèque standard se protège elle-même contre les accès concurrents à partir de plusieurs threads, afin de rendre vos programmes portables vers d'autres environnements.

    Les itérateurs utilisés par les conteneurs sont tous au moins du type ForwardIterator. En pratique, cela signifie que l'on peut parcourir les itérateurs du premier au dernier élément, séquentiellement. Cependant, la plupart des conteneurs disposent d'itérateurs au moins bidirectionnels, et peuvent donc être parcourus dans les deux sens. Les conteneurs qui disposent de ces propriétés sont appelés des conteneurs réversibles.

    Les conteneurs réversibles disposent, en plus des itérateurs directs, d'itérateurs inverses. Ces itérateurs sont repectivement de type reverse_iterator et const_reverse_iterator. Leur initialisation peut être réalisée à l'aide de la fonction rbegin, et leur valeur de fin peut être récupérée à l'aide de la fonction rend.

    17.1.2. Définition des types de données relatifs aux objets contenus

    Outre les types d'itérateurs, les conteneurs définissent également des types spécifiques aux données qu'ils contiennent. Ces types de données permettent de manipuler les données des conteneurs de manière générique, sans avoir de connaissance précises sur la nature réelle des objets qu'ils stockent. Ils sont donc couramment utilisés par les algorithmes de la bibliothèque standard.

    Le type réellement utilisé pour stocker les objets dans un conteneur n'est pas toujours le type template utilisé pour instancier ce conteneur. En effet, certains conteneurs associatifs stockent les clefs des objets avec la valeur des objets eux-mêmes. Ils utilisent pour cela la classe pair, qui permet de stocker, comme on l'a vu en section 14.2.2, des couples de valeurs. Le type des données stockées par ces conteneurs est donc plus complexe que le simple type template par lequel ils sont paramétrés.

    Afin de permettre l'uniformisation des algorithmes travaillant sur ces types de données, les conteneurs définissent tous le type value_type dans leur classe template. C'est en particulier ce type qu'il faut utiliser lors des insertions d'éléments dans les conteneurs. Bien entendu, pour la plupart des conteneurs, et pour toutes les séquences, le type value_type est effectivement le même type que le type template par lequel les conteneurs sont paramétrés.

    Les conteneurs définissent également d'autres types permettant de manipuler les données qu'ils stockent. En particulier, le type reference est le type des références sur les données, et le type const_reference est le type des références constantes sur les données. Ces types sont utilisés par les méthodes des conteneurs qui permettent d'accéder à leurs données.

    17.1.3. Spécification de l'allocateur mémoire à utiliser

    Toutes les classes template des conteneurs de la bibliothèque standard utilisent la notion d'allocateur pour réaliser les opérations de manipulation de la mémoire qu'elles doivent effectuer lors du stockage de leurs éléments ou lors de l'application d'algorithmes spécifiques au conteneur. Le type des allocateurs peut être spécifié dans la liste des paramètres template des conteneurs, en marge du type des données contenues. Les constructeurs des conteneurs prennent tous un paramètre de ce type, qui sera l'allocateur mémoire utilisé pour ce conteneur. Ainsi, il est possible de spécifier un allocateur spécifique pour chaque conteneur, qui peut être particulièrement optimisé pour le type des données gérées par ce conteneur.

    Toutefois, le paramètre template spécifiant la classe de l'allocateur mémoire à utiliser dispose d'une valeur par défaut, qui représente l'allocateur standard de la bibliothèque allocator<T>. Il n'est donc pas nécessaire de spécifier cet allocateur lors de l'instanciation d'un conteneur. Cela rend plus simple l'utilisation de la bibliothèque standard C++ pour ceux qui ne désirent pas développer eux-même un allocateur mémoire. Par exemple, la déclaration template du conteneur list est la suivante :

    template <class T, class Allocator = allocator<T> >
    Il est donc possible d'instancier une liste d'entiers simplement en ne spécifiant que le type des objets contenus, en l'occurrence, des entiers :
    typedef list<int> liste_entier;

    De même, le paramètre des constructeurs permettant de spécifier l'allocateur à utiliser pour les conteneurs dispose systématiquement d'une valeur par défaut, qui est l'instance vide du type d'allocateur spécifié dans la liste des paramètres template. Par exemple, la déclaration du constructeur le plus simple de la classe list est la suivante :

    template <class T, class Allocator>
    list<T, Allocator>::list(const Allocator & = Allocator());
    Il est donc parfaitement légal de déclarer une liste d'entier simplement de la manière suivante :
    liste_entier li;

    Note : Il est peut-être bon de rappeler que toutes les instances d'un allocateur accèdent à la même mémoire. Ainsi, il n'est pas nécessaire, en général, de préciser l'instance de l'allocateur dans le constructeur des conteneurs. En effet, le paramètre par défaut fourni par la bibliothèque standard n'est qu'une instance parmi d'autres qui permet d'accéder à la mémoire gérée par la classe de l'allocateur fournie dans la liste des paramètres template.

    Si vous désirez spécifier une classe d'allocateur différente de celle de l'allocateur standard, vous devrez faire en sorte que cette classe implémente toutes les méthodes des allocateurs de la bibliothèque standard. La notion d'allocateur a été détaillée dans la section 13.6.

    17.1.4. Opérateurs de comparaison des conteneurs

    Les conteneurs disposent d'opérateurs de comparaison permettant d'établir des relations d'équivalence ou des relations d'ordre entre eux.

    Les conteneurs peuvent tous être comparés directement avec les opérateurs == et !=. La relation d'égalité entre deux conteneurs est définie par le respect des deux propriétés suivantes :

    • les deux conteneurs doivent avoir la même taille ;

    • leurs éléments doivent être identiques deux à deux.

    Si le type des objets contenus dispose des opérateurs d'infériorité et de supériorités strictes, les mêmes opérateurs seront également définis pour le conteneur. Ces opérateurs utilisent l'ordre lexicographique pour déterminer le classement entre deux conteneurs. Autrement dit, l'opérateur d'infériorité compare les éléments des deux conteneurs un à un, et fixe son verdict dès la première différence constatée. Si un conteneur est un sous-ensemble du deuxième, le conteneur le plus petit est celui qui est inclus dans l'autre.

    Note : Remarquez que la définition des opérateurs de comparaison d'infériorité et de supériorité existe quel que soit le type des données que le conteneur peut stocker. Cependant, comme les conteneurs sont définis sous la forme de classes template, ces méthodes ne sont instanciées que si elles sont effectivement utilisées dans les programmes. Ainsi, il est possible d'utiliser les conteneurs même sur des types de données pour lesquels les opérateurs d'infériorité et de supériorité ne sont pas définis. Cependant, cette utilisation provoquera une erreur de compilation, car le compilateur cherchera à instancier les opérateurs à ce moment.

    17.1.5. Méthodes d'intérêt général

    Enfin, les conteneurs disposent de méthodes générales permettant d'obtenir des informations sur leurs propriétés. En particulier, le nombre d'éléments qu'ils contiennent peut être déterminé grâce à la méthode size. La méthode empty permet de déterminer si un conteneur est vide ou non. La taille maximale que peut prendre un conteneur est indiquée quant à elle par la méthode max_size. Pour finir, tous les conteneurs disposent d'une méthode swap, qui prend en paramètre un autre conteneur du même type et qui réalise l'échange des données des deux conteneurs. On utilisera de préférence cette méthode à toute autre technique d'échange car seules les références sur les structures de données des conteneurs sont échangées avec cette fonction, ce qui garantit une complexité indépendante de la taille des conteneurs.



    votre commentaire
  • 16.3. Personnalisation des mécanismes de localisation

    Les mécanismes de localisation ont été conçus de telle sorte que le programmeur peut, s'il le désire (et s'il en a réellement le besoin), personnaliser leur fonctionnement. Ainsi, il est parfaitement possible de définir de nouvelles facettes, par exemple pour permettre la localisation des types de données complémentaires définis par le programme. De même, il est possible de redéfinir les méthodes virtuelles des classes de gestion des facettes standards de la bibliothèque et de remplacer les facettes originales par des facettes personnalisées. Cependant, il faut bien reconnaître que la manière de procéder n'est pas très pratique, et en fait les mécanismes internes de gestion des facettes semblent être réservés aux classes et aux méthodes de la bibliothèque standard elle-même.

    16.3.1. Création et intégration d'une nouvelle facette

    Comme il l'a été expliqué dans la section 16.1, une facette n'est rien d'autre qu'une classe dérivant de la classe locale::facet et contenant une donnée membre statique id. Cette donnée membre est utilisée par les classes de locale pour identifier le type de la facette et pour l'intégrer dans le mécanisme de gestion des facettes standards.

    L'exemple suivant montre comment on peut réaliser deux facettes permettant d'encapsuler les spécificités d'un type de donnée défini par le programme, le type answer_t. Ce type est supposé permettre la création de variables contenant la réponse de l'utilisateur à une question. Ce n'est rien d'autre qu'une énumération contenant les valeurs no (pour la réponse négative), yes (pour l'affirmative), all (pour répondre par l'affirmative pour tout un ensemble d'éléments) et none (pour répondre par la négative pour tout un ensemble d'éléments).

    Dans cet exemple, deux facettes sont définies : la facette answerpunct, qui prend en charge la localisation des noms des différentes valeurs de l'énumération answer_t, et la facette answer_put, qui prend en charge le formatage des valeurs de cette énumération dans un flux standard. L'opérateur operator<< est également défini, afin de présenter la manière dont ces facettes peuvent être utilisées. La facette answer_get et l'opérateur correspondant operator>> n'ont pas été définis et sont laissés en exercice pour le lecteur intéressé.

    Exemple 16-6. Définition de nouvelles facettes

    #include <iostream>
    #include <locale>

    using namespace std;

    // Nouveau type de donnée permettant de gérer les réponses
    // aux questions (yes / no / all / none) :
    enum answer_t
    {
    no, yes, all, none
    };

    // Facette prenant définissant les noms des réponses :
    template <class charT>
    class answerpunct : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef basic_string<charT> string_type;

    // L'identifiant de la facette :
    static locale::id id;

    // Le constructeur :
    answerpunct(size_t refs = 0) : locale::facet(refs)
    {
    }

    // Les méthodes permettant d'obtenir les noms des valeurs :
    string_type yesname() const
    {
    return do_yesname();
    }

    string_type noname() const
    {
    return do_noname();
    }

    string_type allname() const
    {
    return do_allname();
    }

    string_type nonename() const
    {
    return do_nonename();
    }

    protected:
    // Le destructeur :
    virtual ~answerpunct()
    {
    }

    // Les méthodes virtuelles :
    virtual string_type do_yesname() const
    {
    return "yes";
    }

    virtual string_type do_noname() const
    {
    return "no";
    }

    virtual string_type do_allname() const
    {
    return "all";
    }

    virtual string_type do_nonename() const
    {
    return "none";
    }
    };

    // Instanciation de l'identifiant de la facette answerpunct :
    template <class charT>
    locale::id answerpunct<charT>::id;

    // Facette prenant en charge le formatage des réponses :
    template <class charT,
    class OutputIterator = ostreambuf_iterator<charT> >
    class answer_put : public locale::facet
    public:
    // Les types de données :
    typedef charT char_type;
    typedef OutputIterator iter_type;
    typedef basic_string<charT> string_type;

    // L'identifiant de la facette :
    static locale::id id;

    // Le constructeur :
    answer_put(size_t refs = 0) : locale::facet(refs)
    {
    }

    // La méthode de formatage publique :
    iter_type put(iter_type i, ios_base &flux,
    char_type remplissage, answer_t valeur) const
    {
    return do_put(i, flux, remplissage, valeur);
    }

    protected:
    // Le destructeur :
    virtual ~answer_put()
    {
    }

    // L'implémentation de la méthode de formatage :
    virtual iter_type do_put(iter_type i, ios_base &flux,
    char_type remplissage, answer_t valeur) const
    {
    // Récupère la facette décrivant les noms de types :
    const answerpunct<charT> &facet =
    use_facet<answerpunct<charT> >(flux.getloc());
    // Récupération du nom qui sera écrit :
    string_type result;
    switch (valeur)
    {
    case yes:
    result = facet.yesname();
    break;
    case no:
    result = facet.noname();
    break;
    case all:
    result = facet.allname();
    break;
    case none:
    result = facet.nonename();
    break;
    }
    // Écriture de la valeur :
    const char *p = result.c_str();
    while (*p != 0)
    {
    *i = *p;
    ++i; ++p;
    }
    return i;
    }
    };

    // Instanciation de l'identifiant de la facette answer_put :
    template <class charT,
    class OutputIterator = ostreambuf_iterator<charT> >
    locale::id answer_put<charT, OutputIterator>::id;

    // Opérateur permettant de formater une valeur
    // de type answer_t dans un flux de sortie :
    template <class charT, class Traits>
    basic_ostream<charT, Traits> &operator<<(
    basic_ostream<charT, Traits> &flux,
    answer_t valeur)
    {
    // Initialisation du flux de sortie :
    typename basic_ostream<charT, Traits>::sentry init(flux);
    if (init)
    {
    // Récupération de la facette de gestion de ce type :
    const answer_put<charT> &facet =
    use_facet<answer_put<charT> >(flux.getloc());
    // Écriture des données :
    facet.put(flux, flux, ' ', valeur);
    }
    return flux;
    }

    int main(void)
    {
    // Crée une nouvelle locale utilisant nos deux facettes :
    locale temp(locale(""), new answerpunct<char>);
    locale loc(temp, new answer_put<char>);
    // Installe cette locale dans le flux de sortie :
    cout.imbue(loc);
    // Affiche quelques valeurs de type answer_t :
    cout << yes << endl;
    cout << no << endl;
    cout << all << endl;
    cout << none << endl;
    return 0;
    }

    Note : Cet exemple, bien que déjà compliqué, passe sous silence un certain nombre de points qu'il faudrait théoriquement prendre en compte pour réaliser une implémentation correcte des facettes et des opérateurs d'insertion et d'extraction des données de type answer_t dans les flux standards. Il faudrait en effet traiter les cas d'erreurs lors des écritures sur le flux de sortie dans la méthode do_put de la facette answer_put, capter les exceptions qui peuvent se produire, corriger l'état du flux d'entrée / sortie au sein de l'opérateur operator<< et relancer ces exceptions.

    De même, les paramètres de la locale ne sont absolument pas pris en compte dans la facette answerpunct, alors qu'une implémentation complète devrait s'en soucier. Pour cela, il faudrait récupérer le nom de la locale incluse dans les flux d'entrée / sortie d'une part, et définir une facette spécialisée answerpunct_byname, en fonction du nom de laquelle les méthodes do_yesname, do_noname, do_allname et do_nonename devraient s'adapter. La section suivante donne un exemple de redéfinition d'une facette existante.

    16.3.2. Remplacement d'une facette existante

    La redéfinition des méthodes de facettes déjà existantes est légèrement plus simple que l'écriture d'une nouvelle facette. En effet, il n'est plus nécessaire de définir la donnée membre statique id. De plus, seules les méthodes qui doivent réellement être redéfinies doivent être récrites.

    L'exemple suivant présente comment un programme peut redéfinir les méthodes do_truename et do_falsename de la facette standard numpunct_byname afin d'en fournir une version localisée en français. Cela permet d'utiliser ces noms français dans les opérations de formatage des flux d'entrée / sortie standards, lorsque le manipulateur boolalpha a été utilisé.

    Exemple 16-7. Spécialisation d'une facette existante

    #include <iostream>
    #include <locale>
    #include <clocale>
    #include <cstring>

    using namespace std;

    // Facette destinée à remplacer numpunct_byname :
    class MyNumpunct_byname :
    public numpunct_byname<char>
    {
    // Les noms des valeurs true et false :
    const char *m_truename;
    const char *m_falsename;

    public:
    MyNumpunct_byname(const char* nom) :
    numpunct_byname<char>(nom)
    {
    // Détermine le nom de la locale active :
    const char *loc = nom;
    if (strcmp(nom, "") == 0)
    {
    // Récupère le nom de la locale globale active :
    loc = setlocale(0, NULL);
    }
    // Prend en charge les noms français :
    if (strcmp(loc, "fr_FR") == 0)
    {
    m_truename = "vrai";
    m_falsename = "faux";
    }
    else
    {
    // Pour les autres locales, utilise les noms anglais :
    m_truename = "true";
    m_falsename = "false";
    }
    }

    protected:
    ~MyNumpunct_byname()
    {
    }

    string do_truename() const
    {
    return m_truename;
    }

    string do_falsename() const
    {
    return m_falsename;
    }
    };

    int main(void)
    {
    // Fixe la locale globale du programme :
    locale::global(locale(""));
    // Crée une nouvelle locale utilisant notre facette :
    locale l(locale(""), new MyNumpunct_byname(""));
    // Installe cette locale dans le flux de sortie :
    cout.imbue(l);
    // Affiche deux booléens :
    cout << boolalpha << true << endl;
    cout << false << endl;
    return 0;
    }

    Note : La classe de base de la facette MyNumpunct_byname est la classe numpunct_byname parce que la facette a besoin de connaître le nom de la locale pour laquelle elle est construite. En effet, aucun autre mécanisme standard ne permet à une facette de récupérer ce nom et donc de s'adapter aux différentes locales existantes. Vous remarquerez que les facettes de formatage n'ont pas besoin de connaître ce nom puisqu'elles peuvent le récupérer grâce à la méthode name de la locale du flux sur lequel elles travaillent.

    La facette MyNumpunct_byname utilise la fonction setlocale de la bibliothèque C pour récupérer le nom de la locale courante si elle est initialisée avec un nom vide. En réalité, elle devrait récupérer ce nom par ses propres moyens et effectuer les traductions des noms des valeurs true et false par elle-même, car cela suppose que la locale globale du programme est initialisée avec le même nom. C'est pour cela que le programme principal commence par appeler la méthode global de la classe local avec comme paramètre une locale anonmyme. Cela dit, les mécanismes permettant à un programme de récupérer les paramètres de la locale définie dans l'environnement d'exécution du programme sont spécifiques à chaque système et ne peuvent donc pas être décrits ici.

    Bien entendu, si d'autres langues que le français devaient être prises en compte, d'autre mécanismes plus génériques devraient également être mis en place pour définir les noms des valeurs true et false afin d'éviter de compliquer exagérément le code de la facette.


    votre commentaire
  • 16.2. Les facettes standards

    Cette section présente l'ensemble des facettes standards définies par la bibliothèque standard. La première partie décrit l'architecture générale à laquelle les facettes standards se conforment, et les parties suivantes donnent une description des principales fonctionnalités fournies par chacune des catégories de facettes.

    16.2.1. Généralités

    Les facettes fournies par la bibliothèque standard sont des classes template paramétrées par le type des caractères sur lesquels elles travaillent. Pour quelques-unes de ces facettes, la bibliothèque standard définit une spécialisation pour les types char ou wchar_t, spécialisation dont le but est d'optimiser les traitements des facettes pour les flux d'entrée / sortie standards sur ces types.

    Certaines de ces facettes ne sont utilisées que pour fournir des informations aux autres parties de la bibliothèque standard C++. D'autres, en revanche, permettent de réaliser les opérations de formatage et d'analyse syntaxique sur lesquelles les flux d'entrée / sortie s'appuient pour implémenter les opérateurs operator<< et operator>>. Ces facettes disposent alors de méthodes put et get qui permettent d'effectuer ces deux types d'opération.

    Les traitements effectués par les facettes doivent prendre en compte les paramètres de leur locale ainsi que les options de formatage stockées dans les flux sur lesquelles les entrées / sorties doivent être effectuées. Pour cela, les facettes doivent disposer d'un moyen de récupérer la locale dont elles font partie ou dont elles doivent utiliser les paramètres. Généralement, les facettes qui réalisent les opérations d'entrée / sortie pour le compte des flux utilisent la méthode getloc de ces derniers pour obtenir la locale dont elles doivent utiliser les paramètres. Les autres facettes ne peuvent pas procéder de la même manière, car elles ne disposent pas forcément d'un objet flux pour déterminer la locale courante. Afin de résoudre ce problème, la bibliothèque standard définit des classes de facettes dérivées et dont le constructeur prend en paramètre le nom de la locale à laquelle ces facettes appartiennent. Ces classes sont donc initialisées, dès leur construction, avec le nom de la locale dans laquelle elles se trouvent, ce qui leur permet éventuellement d'effectuer des traitements dépendants de cette locale. Les noms de ces classes de facettes dérivées sont les mêmes que ceux de leurs classes de base, à ceci près qu'ils sont suffixés par la chaîne « _byname ». Par exemple, la facette ctype, qui, comme on le verra plus loin, permet de classer les caractères selon leur nature, dispose d'une classe dérivée ctype_byname dont le constructeur prend en paramètre le nom de la locale dont la facette fait partie.

    Note : Les implémentations de la bibliothèque standard fournies avec les environnements de développement C++ ne sont pas tenues de fournir ces facettes pour chaque locale existante dans le monde. En réalité, quasiment aucun environnement ne le fait à l'heure actuelle. En revanche, toutes les facettes standards doivent au moins être fournies et fonctionner correctement avec les locales "C" et "".

    Les facettes sont écrites de telle manière qu'elles peuvent facilement être remplacées par des facettes plus spécifiques. Ainsi, leurs méthodes publiques appellent toutes des méthodes virtuelles, qui peuvent parfaitement être redéfinies par des classes dérivées désirant remplacer l'un des traitements effectués par la bibliothèque standard.

    Généralement, les noms des méthodes virtuelles sont les mêmes que ceux des méthodes publiques qui les utilisent, précédés du préfixe « do_ ». Par exemple, si une facette fournit une méthode publique nommée put, la méthode virtuelle appelée par celle-ci se nommera do_put. La manière de redéfinir les méthodes d'une facette existante et de remplacer cette facette par une de ses classes dérivées dans une locale sera décrite en détail dans la section 16.3.2.

    16.2.2. Les facettes de manipulation des caractères

    La bibliothèque standard définit deux facettes permettant de manipuler les caractères. La première facette, la facette ctype, fournit les fonctions permettant de classer les caractères en différentes catégories. Ces catégories comprennent les lettres, les chiffres, les caractères imprimables, les caractères graphiques, etc. La deuxième facette permet quant à elle d'effectuer les conversions entre les différents types existants d'encodage de caractères. Il s'agit de la facette code_cvt.

    16.2.2.1. La facette ctype

    La facette ctype dérive d'une classe de base dans laquelle sont définies les différentes catégories de caractères. Cette classe est déclarée comme suit dans l'en-tête locale :

    class ctype_base
    {
    public:
    enum mask
    {
    space = SPACE_VALUE, print = PRINT_VALUE,
    cntrl = CNTRL_VALUE, alpha = ALPHA_VALUE,
    digit = DIGIT_VALUE, xdigit = XDIGIT_VALUE,
    upper = UPPER_VALUE, lower = LOWER_VALUE,
    punct = PUNCT_VALUE,
    alnum = alpha | digit, graph = alnum | punct
    };
    };
    Les valeurs numériques utilisées par cette énumération sont définies de telle manière que les constantes de type mask constituent un champ de bits. Ainsi, il est possible de définir des combinaisons entre ces valeurs, certains caractères pouvant appartenir à plusieurs catégories en même temps. Deux combinaisons standards sont d'ailleurs définies, alnum, qui caractérise les caractères alphanumériques, et graph, qui représente tous les caractères alphanumériques et de ponctuation. Les autres constantes permettent de caractériser les caractères selon leur nature et leur signification est en général claire. La seule constante qui dont l'interprétation n'est pas immédiate est la constante xdigit, qui identifie tous les caractères pouvant servir de chiffre dans la notation des nombres hexadécimaux. Cela comprend les chiffres normaux et les lettres 'A' à 'F'.

    La classe template ctype quant à elle est déclarée comme suit dans l'en-tête locale :

    template <class charT>
    class ctype : public locale::facet, public ctype_base
    {
    public:
    // Les types de données :
    typedef charT char_type;

    // Le constructeur :
    explicit ctype(size_t refs = 0);

    // Les méthode de classification :
    bool is(mask m, charT c) const;
    const charT *is(const charT *premier, const charT *dernier,
    mask *vecteur) const;
    const charT *scan_is(mask m,
    const charT *premier, const charT *dernier) const;
    const charT *scan_not(mask m,
    const charT *premier, const charT *dernier) const;
    charT toupper(charT c) const;
    const charT *toupper(const charT *premier, const charT *dernier) const;
    charT tolower(charT c) const;
    const charT *tolower(const charT *premier, const charT *dernier) const;
    charT widen(char c) const;
    const charT *widen(const char *premier, const char *dernier,
    charT *destination) const;
    char narrow(charT c, char defaut) const;
    const char *narrow(const charT *premier, const charT *dernier,
    char defaut, char *destination) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Comme pour toutes les facettes standards, les méthodes publiques délèguent leur travail à des méthodes virtuelles déclarées en zone protégée dont le nom est celui de la méthode publique préfixé par la chaîne de caractères « do_ ». Ces méthodes peuvent être redéfinies par les classes dérivées de la facette ctype et font donc partie de l'interface des facettes standards. Cependant, elles ne sont pas représentées dans la déclaration donnée ci-dessus par souci de simplicité. Leur sémantique est exactement la même que celle des méthodes publiques correspondantes. Nous verrons dans la section 16.3.2 la manière de procéder pour redéfinir certaines des méthodes des facettes standards.

    Les méthodes scan_is et scan_not permettent de rechercher un caractère selon un critère particulier dans un tableau de caractères. La méthode scan_is recherche le premier caractère qui est du type indiqué par son paramètre m, et la méthode scan_not le premier caractère qui n'est pas de ce type. Ces deux méthodes prennent en paramètre un pointeur sur le premier caractère du tableau dans lequel la recherche doit s'effectuer et le pointeur suivant l'emplacement du dernier caractère de ce tableau. Elles renvoient toutes les deux un pointeur référençant le caractère trouvé, ou le pointeur de fin si aucun caractère ne vérifie le critère spécifié.

    Les autres méthodes de la facette ctype sont fournies sous deux versions. La première permet d'effectuer une opération sur un caractère unique et la deuxième permet de reproduire cette opération sur une séquence de caractères consécutifs. Dans ce dernier cas, les caractères sur lesquels l'opération peut être effectuée sont spécifiés à l'aide de deux pointeurs, l'un sur le premier caractère et l'autre sur le caractère suivant le dernier caractère de la séquence, comme il est d'usage de le faire dans tous les algorithmes de la bibliothèque standard.

    Les deux méthodes is permettent donc respectivement de déterminer si un caractère est du type indiqué par le paramètre m ou non, ou d'obtenir la suite des descriptions de chaque caractère dans le tableau de valeur de type mask pointé par le paramètre vecteur. De même, les méthodes toupper et tolower permettent respectivement de convertir un caractère unique ou tous les caractères d'un tableau en majuscule ou en minuscule. La méthode widen permet de transtyper un caractère ou tous les caractères d'un tableau de type char en caractères du type par lequel la classe ctype est paramétrée. Enfin, les méthodes narrow permettent de réaliser l'opération inverse, ce qui peut provoquer une perte de données puisque le type char est le plus petit des types de caractères qui puisse exister. Il est donc possible que le transtypage ne puisse se faire, dans ce cas, les méthodes narrow utilisent la valeur par défaut spécifiée par le paramètre defaut.

    Exemple 16-2. Conversion d'une wstring en string

    #include <iostream>
    #include <string>
    #include <locale>

    using namespace std;

    int main(void)
    {
    // Fixe la locale globale aux préférences de l'utilisateur :
    locale::global(locale(""));
    // Lit une chaîne de caractères larges :
    wstring S;
    wcin >> S;
    // Récupère la facette ctype<wchar_t> de la locale courante :
    const ctype<wchar_t> &f =
    use_facet<ctype<wchar_t> >(locale());
    // Construit un tampon pour recevoir le résultat de la conversion :
    size_t l = S.length() + 1;
    char *tampon = new char[l];
    // Effectue la conversion :
    f.narrow(S.c_str(), S.c_str() + l, 'E', tampon);
    // Affiche le résultat :
    cout << tampon << endl;
    delete[] tampon;
    return 0;
    }

    Note : Les conversions effectuées par les méthodes narrow et widen ne travaillent qu'avec les représentations de caractères classiques du langage C++. Cela signifie que les caractères sont tous représentés par une unique valeur de type char ou wchar_t (ces méthodes n'utilisent donc pas de représentation des caractères basées sur des séquences de caractères de longueurs variables). La méthode narrow de l'exemple précédent écrit donc autant de caractères dans le tampon destination qu'il y en a dans la chaîne à convertir.

    Vous constaterez que l'utilisation de la méthode is pour déterminer la nature des caractères peut être relativement fastidieuse, car il faut récupérer la facette ctype, déterminer la valeur du masque à utiliser, puis appeler la méthode. La bibliothèque standard définit donc un certain nombre de fonctions globales utilitaires dans l'en-tête locale :

    template <class charT> bool isspace (charT c, const locale &l) const;
    template <class charT> bool isprint (charT c, const locale &l) const;
    template <class charT> bool iscntrl (charT c, const locale &l) const;
    template <class charT> bool isupper (charT c, const locale &l) const;
    template <class charT> bool islower (charT c, const locale &l) const;
    template <class charT> bool isalpha (charT c, const locale &l) const;
    template <class charT> bool isdigit (charT c, const locale &l) const;
    template <class charT> bool ispunct (charT c, const locale &l) const;
    template <class charT> bool isxdigit(charT c, const locale &l) const;
    template <class charT> bool isalnum (charT c, const locale &l) const;
    template <class charT> bool isgraph (charT c, const locale &l) const;
    template <class charT> charT toupper(charT c, const locale &l) const;
    template <class charT> charT tolower(charT c, const locale &l) const;

    L'utilisation de ces fonctions ne doit pas poser de problème particulier. Elles prennent toutes en premier paramètre le caractère à caractériser et en deuxième paramètre la locale dont la facette ctype doit être utilisée pour réaliser cette caractérisation. Chaque fonction permet de tester le caractère pour l'appartenance à l'une des catégories de caractères définies dans la classe de base ctype_base. Notez cependant que si un grand nombre de caractères doivent être caractérisés pour une même locale, il est plus performant d'obtenir la facette ctype de cette locale une bonne fois pour toutes et d'effectuer les appels à la méthode is en conséquence.

    La classe ctype étant une classe template, elle peut être utilisée pour n'importe quel type de caractère a priori. Toutefois, il est évident que cette classe peut être optimisée pour les types de caractère simples, et tout particulièrement pour le type char, parce qu'il ne peut pas prendre plus de 256 valeurs différentes. La bibliothèque standard définit donc une spécialisation totale de la classe template ctype pour le type char. L'implémentation de cette spécialisation se base sur un tableau de valeurs de type mask indexée par les valeurs que peuvent prendre les variables de type char. Ce tableau permet donc de déterminer rapidement les caractéristiques de chaque caractère existant. Le constructeur de cette spécialisation diffère légèrement du constructeur de sa classe template car il peut prendre en paramètre un pointeur sur ce tableau de valeurs et un booléen indiquant si ce tableau doit être détruit automatiquement par la facette lorsqu'elle est elle-même détruite ou non. Ce constructeur prend également en troisième paramètre une valeur de type entier indiquant, comme pour toutes les facettes standards, si la locale doit prendre en charge la gestion de la durée de vie de la facette ou non. Les autres méthodes de cette spécialisation sont identiques aux méthodes de la classe template de base et ne seront donc pas décrites ici.

    16.2.2.2. La facette codecvt

    La facette codecvt permet de réaliser les opérations de conversion d'un mode de représentation des caractères à un autre. En général, en informatique, les caractères sont codés par des nombres. Le type de ces nombres, ainsi que la manière de les utiliser, peut varier grandement d'une représentation à une autre, et les conversions peuvent ne pas se faire simplement. Par exemple, certaines représentations codent chaque caractère avec une valeur unique du type de caractère utilisé, mais d'autres codent les caractères sur des séquences de longueur variable. On ne peut dans ce cas bien entendu pas convertir directement une représentation en une autre, car l'interprétation que l'on peut faire des nombres représentant les caractères dépend du contexte déterminé par les nombres déjà lus. Les opérations de conversion ne sont donc pas toujours directes.

    De plus, dans certains encodages à taille variable, l'interprétation des caractères peut dépendre des caractères déjà convertis. La facette codecvt maintient donc un état pendant les conversions qu'elle effectue, état qui lui permet de reprendre la conversion d'une séquence de caractères dans le cas de conversions réalisées en plusieurs passes. Bien entendu, tous les encodages ne nécessitent pas forcément le maintien d'un tel état. Cependant, certains l'exigent et il faut donc toujours le prendre en compte dans les opérations de conversion si l'on souhaite que le programme soit portable. Pour les séquences de caractères à encodage variable utilisant le type de caractère de base char, le type de la variable d'état permettant de stocker l'état courant du convertisseur est le type mbstate_t. D'autres types peuvent être utilisés pour les séquences basées sur des types de caractères différents du type char, mais en général, tous les encodages à taille variable se basent sur ce type. Quoi qu'il en soit, la classe codecvt définit un type de donnée capable de stocker l'état d'une conversion partielle. Ce type est le type state_type, qui pourra donc toujours être récupéré dans la classe codecvt. La variable d'état du convertisseur devra être systématiquement fournie aux méthodes de conversion de la facette codecvt et devra bien entendu être initialisée à sa valeur par défaut au début de chaque nouvelle conversion.

    Note : La facette codecvt permet de réaliser les conversions d'une représentation des caractères à une autre, mais n'a pas pour but de changer l'encodage des caractères, c'est-à-dire l'association qui est faite entre les séquences de nombres et les caractères. Cela signifie que la facette codecvt permet par exemple de convertir des chaînes de caractères larges wchar_t en séquences de longueurs variables de caractères de type char, mais elle ne permet pas de passer d'une page de codes à une autre.

    La facette codecvt dérive d'une classe de base nommée codecvt_base. Cette classe définit les différents résultats que peuvent avoir les opérations de conversion. Elle est déclarée comme suit dans l'en-tête locale :

    class codecvt_base
    {
    public:
    enum result
    {
    ok, partial, error, noconv
    };
    };

    Comme vous pouvez le constater, une conversion peut se réaliser complètement (code de résultat ok), partiellement par manque de place dans la séquence destination ou par manque de données en entrées (code partial), ou pas du tout, soit en raison d'une erreur de conversion (code d'erreur error), soit parce qu'aucune conversion n'est nécessaire (code de résultat noconv).

    La classe template codecvt elle-même est définie comme suit dans l'en-tête locale :

    template <class internT, class externT, class stateT>
    class codecvt : public locale::facet, public codecvt_base
    {
    public:
    // Les types de données :
    typedef internT intern_type;
    typedef externT extern_type;
    typedef stateT state_type;

    // Le constructeur :
    explicit codecvt(size_t refs=0);

    // Les fonctions de conversion :
    result out(stateT &etat, const internT *premier,
    const internT *dernier, const internT *&suiv_source,
    externT *dernier, externT *limite, externT *&suiv_dest) const;
    result in(stateT &etat, const externT *premier,
    const externT *dernier, const externT *&suiv_source,
    internT *dernier, internT *limite, internT *&suiv_dest) const;
    result unshift(stateT &etat,
    externT *dernier, externT *limite, externT *&suiv_dest) const;
    int length(const stateT &etat,
    const externT *premier, const externT *dernier, size_t max) const;
    int max_length() const throw();
    int encoding() const throw();
    bool always_noconv() const throw();

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Cette classe template est paramétrée par le type de caractère interne à la classe codecvt, par un deuxième type de caractère qui sera par la suite dénommé type externe, et par le type des variables destinées à recevoir l'état courant d'une conversion. Les implémentations de la bibliothèque standard doivent obligatoirement instancier cette classe template pour les types char et wchar_t. Le type de gestion de l'état des conversions utilisé est alors le type prédéfini mbstate_t, qui permet de conserver l'état des conversions entre le type natif wchar_t et les séquences de caractères simples à taille variable. Ainsi, vous pourrez toujours utiliser les instances codecvt<wchar_t, char, mbstate_t> et codecvt<char, char, mbstate_t> de la facette codecvt dans vos programmes. Si vous désirez réaliser des conversions pour d'autres types de caractères, vous devrez fournir vous-même des spécialisations de la facette codecvt.

    Les méthodes in et out permettent respectivement, comme leurs signatures l'indiquent, de réaliser les conversions entre les types interne et externe et vice versa. Elles prennent toutes deux sept paramètres. Le premier paramètre est une référence sur la variable d'état qui devra être fournie à chaque appel lors de conversions successives d'un même flux de données. Cette variable est destinée à recevoir l'état courant de la conversion et permettra aux appels suivants de convertir correctement les caractères suivants du flux d'entrée. Les deux paramètres suivants permettent de spécifier la séquence de caractères à convertir. Ils doivent contenir le pointeur sur le début de la séquence et le pointeur sur le caractère suivant le dernier caractère de la séquence. Le quatrième paramètre est un paramètre de retour, la fonction lui affectera la valeur du pointeur où la conversion s'est arrêtée. Une conversion peut s'arrêter à cause d'une erreur ou tout simplement parce que le tampon destination ne contient pas assez de place pour accueillir un caractère de plus. Ce pointeur pourra être utilisé dans un appel ultérieur comme pointeur de départ avec la valeur de la variable d'état à l'issue de la conversion pour effectuer la suite de cette conversion. Enfin, les trois derniers paramètres spécifient le tampon destination dans lequel la séquence convertie doit être écrite. Ils permettent d'indiquer le pointeur de début de ce tampon, le pointeur suivant le dernier emplacement utilisable, et un pointeur de retour qui indiquera la dernière position écrite par l'opération de conversion. Ces deux méthodes renvoient une des constantes de l'énumération result définie dans la classe de base codecvt_base pour indiquer comment la conversion s'est effectuée. Si aucune conversion n'est nécessaire, les pointeurs sur les caractères suivants sont initialisés à la valeur des pointeurs de début de séquence et aucune écriture n'a lieu dans le tampon destination.

    Exemple 16-3. Conversion d'une chaîne de caractères larges en chaîne à encodage variable

    #include <iostream>
    #include <string>
    #include <locale>

    using namespace std;

    int main(void)
    {
    // Fixe la locale globale :
    locale::global(locale(""));
    // Lit une ligne :
    wstring S;
    getline(wcin, S);
    // Récupère la facette de conversion vers wchar_t :
    const codecvt<wchar_t, char, mbstate_t> &f =
    use_facet<codecvt<wchar_t, char, mbstate_t> >(locale());
    // Effectue la conversion :
    const wchar_t *premier = S.c_str();
    const wchar_t *dernier = premier + S.length();
    const wchar_t *suivant = premier;
    string s;
    char tampon[10];
    char *fincvt = tampon;
    codecvt_base::result r;
    mbstate_t etat = mbstate_t();
    while (premier != dernier)
    {
    // Convertit un morceau de la chaîne :
    r = f.out(etat, premier, dernier, suivant,
    tampon, tampon+10, fincvt);
    // Vérifie les erreurs possibles :
    if (r == codecvt_base::ok || r == codecvt_base::partial)
    cout << "." << flush;
    else if (r == codecvt_base::noconv)
    {
    cout << "conversion non nécessaire" << endl;
    break;
    }
    else if (r == codecvt_base::error)
    {
    cout << "erreur" << endl;
    cout << suivant - premier << endl;
    cout << fincvt - tampon << endl;
    break ;
    }
    // Récupère le résultat et prépare la conversion suivante :
    s.append(tampon, fincvt - tampon);
    premier = suivant;
    }
    cout << endl;
    // Affiche le résultat :
    cout << s << endl;
    return 0;
    }

    Note : Si l'on désire effectuer une simple conversion d'une chaîne de caractères de type wchar_t en chaîne de caractères C classique, on cherchera plutôt à utiliser la méthode narrow de la facette ctype présentée dans la section précédente. En effet, la facette codecvt utilise, a priori, une séquence de caractères avec un encodage à taille variable, ce qui ne correspond pas à la représentation des chaînes de caractères C classiques, pour lesquelles chaque valeur de type char représente un caractère.

    Il est possible de compléter une séquence de caractères à encodage variable de telle sorte que la variable d'état du convertisseur soit réinitialisée. Cela permet de terminer une chaîne de caractères partiellement convertie, ce qui en pratique revient à compléter la séquence de caractères avec les données qui représenteront le caractère nul terminal. Cette opération peut être réalisée à l'aide de la méthode unshift de la facette codecvt. Cette méthode prend en paramètre une référence sur la variable d'état du convertisseur, ainsi que les pointeurs de début et de fin du tampon dans lequel les valeurs à ajouter sont écrites. Le dernier paramètre de la méthode unshift est une référence sur un pointeur qui recevra l'adresse suivant celle la dernière valeur écrite par la méthode si l'opération se déroule correctement.

    Il va de soi que la détermination de la longueur d'une chaîne de caractères dont les caractères ont une représentation à taille variable n'est pas simple. La facette codecvt comporte donc une méthode length permettant de calculer, en nombre de caractères de type intern_type, la longueur d'une séquence de caractères de type extern_type. Cette méthode prend en paramètre la variable d'état du convertisseur ainsi que les pointeurs spécifiant la séquence de caractères dont la longueur doit être calculée. Le dernier paramètre est la valeur maximale que la fonction peut retourner. Elle permet de limiter la détermination de la longueur de la séquence source à une borne maximale, par exemple la taille d'un tampon destination. La valeur retournée est bien entendu la longueur de cette séquence ou, autrement dit, le nombre de valeurs de type intern_type nécessaires pour stocker le résultat de la conversion que la méthode in ferait avec les mêmes paramètres. D'autre part, il est possible de déterminer le nombre maximal de valeurs de type intern_type nécessaires pour représenter un unique caractère représenté par une séquence de caractères de type extern_type. Pour cela, il suffit d'appeler la méthode max_length de la facette codecvt.

    Exemple 16-4. Détermination de la longueur d'une chaîne de caractères à encodage variable

    #include <iostream>
    #include <string>
    #include <locale>
    #include <limits>

    using namespace std;

    int main(void)
    {
    // Fixe la locale globale :
    locale::global(locale(""));
    // Lit une ligne :
    string s;
    getline(cin, s);
    // Récupère la facette de conversion vers wchar_t :
    const codecvt<wchar_t, char, mbstate_t> &f =
    use_facet<codecvt<wchar_t, char, mbstate_t> >(locale());
    // Affiche la longueur de la chaîne d'entrée :
    int l1 = s.length();
    // Calcule la longueur de la ligne en wchar_t :
    mbstate_t etat = mbstate_t();
    int l2 = f.length(etat, s.c_str(), s.c_str() + l1,
    numeric_limits<size_t>::max());
    // Affiche les deux longueurs :
    cout << l1 << endl;
    cout << l2 << endl;
    return 0;
    }

    Comme on l'a déjà indiqué ci-dessus, toutes les représentations des caractères ne sont pas à taille variable et toutes les représentations ne nécessitent pas forcément l'utilisation d'une variable d'état de type state_type. Vous pouvez déterminer dynamiquement si le mode de représentation des caractères du type intern_type utilise un encodage à taille variable ou non à l'aide de la méthode encoding. Cette méthode renvoie -1 si la représentation des caractères de type extern_type dépend de l'état du convertisseur, ou le nombre de caractères de type extern_type nécessaires au codage d'un caractère de type intern_type si ce nombre est constant. Si la valeur renvoyée est 0, ce nombre n'est pas constant, mais, contrairement à ce qui se passe lorsque la valeur renvoyée est -1, ce nombre ne dépend pas de la valeur de la variable d'état du convertisseur.

    Enfin, certains modes de représentation des caractères sont compatibles, voire franchement identiques. Dans ce cas, jamais aucune conversion n'est réalisée, et les méthodes in et out renvoient toujours noconv. C'est par exemple le cas de la spécialisation codecvt<char, char, mbstate_t> de la facette codecvt. Vous pouvez déterminer si une facette effectuera des conversions ou non en appelant la méthode always_noconv. Elle retourne true si jamais aucune conversion ne se fera et false sinon.

    16.2.3. Les facettes de comparaison de chaînes

    Les chaînes de caractères sont généralement classées par ordre alphabétique, ou, plus précisément, dans l'ordre lexicographique. L'ordre lexicographique est l'ordre défini par la séquence des symboles lexicaux utilisés (c'est-à-dire les symboles utilisés pour former les mots du langage, donc, en pratique, les lettres, les nombres, la ponctuation, etc.). Cet ordre est celui qui est défini par la comparaison successive des caractères des deux chaînes à comparer, le premier couple de caractères différents permettant de donner un jugement de classement. Ainsi, les chaînes les plus petites au sens de l'ordre lexicographique sont les chaînes qui commencent par les premiers symboles du lexique utilisé. Cette manière de procéder suppose bien entendu que les symboles utilisés pour former les mots du lexique sont classés dans un ordre correct. Par exemple, il faut que la lettre 'a' apparaisse avant la lettre 'b', qui elle-même doit apparaître avant la lettre 'c', etc.

    Malheureusement, cela n'est pas si simple, car cet ordre n'est généralement pas celui utilisé par les pages de codes d'une part, et il existe toujours des symboles spéciaux dont la classification nécessite un traitement spécial d'autre part. Par exemple, les caractères accentués sont généralement placés en fin de page de code et apparaissent donc à la fin de l'ordre lexicographique, ce qui perturbe automatiquement le classement des chaînes de caractères contenant des accents. De même, certaines lettres sont en réalité des compositions de lettres et doivent être prises en compte en tant que telles dans les opérations de classement. Par exemple, la lettre 'æ' doit être interprétée comme un 'a' suivi d'un 'e'. Et que dire du cas particulier des majuscules et des minuscules ?

    Comme vous pouvez le constater, il n'est pas possible de se baser uniquement sur l'ordre des caractères dans leur page de code pour effectuer les opérations de classement de chaînes de caractères. De plus, il va de soi que l'ordre utilisé pour classer les symboles lexicographiques dépend de ces symboles et donc de la locale utilisé. La bibliothèque standard fournit donc une facette prenant en compte tous ces paramètres : la classe template collate.

    Le principe de fonctionnement de la facette collate est de transformer les chaînes de caractères utilisant les conventions de la locale à laquelle la facette appartient en une chaîne de caractères indépendante de la locale, comprenant éventuellement des codes de contrôle spéciaux pour les caractères spécifiques à cette locale. Les chaînes de caractères ainsi transformées peuvent alors être comparées entre elles directement, avec les méthodes de comparaison classique de chaînes de caractères qui utilisent l'ordre lexicographique du jeu de caractères du langage C. La transformation est effectuée de telle manière que cette comparaison produit le même résultat que la comparaison tenant compte de la locale des chaînes de caractères non transformées.

    La facette collate est déclarée comme suit dans l'en-tête locale :

    template <class charT>
    class collate : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit collate(size_t refs = 0);

    // Les méthodes de comparaison de chaînes :
    string_type transform(const charT *debut, const charT *fin) const;
    int compare(const charT *deb_premier, const charT *fin_premier,
    const charT *deb_deuxieme, const charT *fin_deuxieme) const;
    long hash(const charT *debut, const charT *fin) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    La méthode transform est la méthode fondamentale de la facette collate. C'est cette méthode qui permet d'obtenir la chaîne de caractères transformée. Elle prend en paramètre le pointeur sur le début de la chaîne de caractères à transformer et le pointeur sur le caractère suivant le dernier caractère de cette chaîne. Elle retourne une basic_string contenant la chaîne transformée, sur laquelle les opérations de comparaison classiques pourront être appliquées.

    Il est possible d'effectuer directement la comparaison entre deux chaînes de caractères, sans avoir à récupérer les chaînes de caractères transformées. Cela peut être réalisé grâce à la méthode compare, qui prend en paramètre les pointeurs de début et de fin des deux chaînes de caractères à comparer et qui renvoie un entier indiquant le résultat de la comparaison. Cet entier est négatif si la première chaîne est inférieure à la deuxième, positif si elle est supérieure, et nul si les deux chaînes sont équivalentes.

    Exemple 16-5. Comparaison de chaînes de caractères localisées

    #include <iostream>
    #include <string>
    #include <locale>

    using namespace std;

    int main(void)
    {
    // Fixe la locale globale :
    locale::global(locale(""));
    // Lit deux lignes en entrée :
    cout << "Entrez la première ligne :" << endl;
    string s1;
    getline(cin, s1);
    cout << "Entrez la deuxième ligne :" << endl;
    string s2;
    getline(cin, s2);
    // Récupère la facette de comparaison de chaînes :
    const collate<char> &f =
    use_facet<collate<char> >(locale());
    // Compare les deux chaînes :
    int res = f.compare(
    s1.c_str(), s1.c_str() + s1.length(),
    s2.c_str(), s2.c_str() + s2.length());
    if (res < 0)
    {
    cout << "\"" << s1 << "\" est avant \"" <<
    s2 << "\"." << endl;
    }
    else if (res > 0)
    {
    cout << "\"" << s1 << "\" est après \"" <<
    s2 << "\"." << endl;
    }
    else
    {
    cout << "\"" << s1 << "\" est égale à \"" <<
    s2 << "\"." << endl;
    } return 0;
    }

    Note : La méthode compare est très pratique pour comparer deux chaînes de caractères de manière ponctuelle. Cependant, on lui préférera la méthode transform si un grand nombre de comparaisons doit être effectué. En effet, il est plus simple de transformer toutes les chaînes de caractères une bonne fois pour toutes et de travailler ensuite directement sur les chaînes transformées. Ce n'est que lorsque les opérations de comparaison auront été terminées que l'on pourra revenir sur les chaînes de caractères initiales. On évite ainsi de faire des transformation à répétition des chaînes à comparer et on gagne ainsi beaucoup de temps. Bien entendu, cela nécessite de conserver l'association entre les chaînes de caractères transformées et les chaînes de caractères initiales, et donc de doubler la consommation mémoire du programme due au chaînes de caractères pendant le traitement de ces chaînes.

    Enfin, il est courant de chercher à déterminer une clef pour chaque chaîne de caractères. Cette clef peut être utilisée pour effectuer une recherche rapide des chaînes de caractères. La méthode hash de la facette collate permet de calculer une telle clef, en garantissant que deux chaînes de caractères identiques au sens de la méthode compare auront la même valeur de clef. On notera cependant que cette clef n'est pas unique, deux chaînes de caractères peuvent avoir deux valeurs de clefs identiques même si la méthode compare renvoie une valeur non nulle. Cependant, ce cas est extrêmement rare, et permet d'utiliser malgré tout des algorithmes de recherche rapide. La seule chose à laquelle il faut faire attention est que ces algorithmes doivent pouvoir supporter les clefs multiples.

    Note : Les clefs à probabilité de recouvrement faible comme celle retournée par la méhtode hash sont généralement utilisées dans les structures de données appelées tables de hachage, ce qui explique le nom donné à cette méthode. Les tables de hachage sont en réalité des tableaux de listes chaînées indexés par la clef de hachage (si la valeur de la clef dépasse la taille du tableau, elle est ramenée dans les limites de celui-ci par une opération de réduction). Ce sont des structures permettant de rechercher rapidement des valeurs pour lesquelles une fonction de hachage simple existe. Cependant, elles se comportent moins bien que les arbres binaires lorsque le nombre d'éléments augmente (quelques milliers). On leur préférera donc généralement les associations de la bibliothèque standard, comme les map et multimap par exemple. Vous pouvez consulter la bibliographie si vous désirez obtenir plus de renseignements sur les tables de hachage et les structures de données en général. Les associations et les conteneurs de la bibliothèque standard seront décrites dans le chapitre 17.

    16.2.4. Les facettes de gestion des nombres

    Les opérations de formatage et les opérations d'interprétation des données numériques dépendent bien entendu des conventions nationales de la locale incluse dans les flux qui effectuent ces opérations. En réalité, ces opérations ne sont pas prises en charge directement par les flux, mais plutôt par les facettes de gestion des nombres, qui regroupent toutes les opérations propres aux conventions nationales.

    La bibliothèque standard définit en tout trois facettes qui interviennent dans les opérations de formatage : une facette utilitaire, qui contient les paramètres spécifiques à la locale, et deux facettes dédiées respectivement aux opérations de lecture et aux opérations d'écriture des nombres.

    16.2.4.1. La facette num_punct

    La facette qui regroupe tous les paramètres de la locale est la facette num_punct. Elle est déclarée comme suit dans l'en-tête locale :

    template <class charT>
    class numpunct : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit numpunct(size_t refs = 0);

    // Les méthodes de lecture des options de formatage des nombres :
    char_type decimal_point() const;
    char_type thousands_sep() const;
    string grouping() const;
    string_type truename() const;
    string_type falsename() const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    La méthode decimal_point permet d'obtenir le caractère qui doit être utilisé pour séparer le chiffre des unités des chiffres après la virgule lors des opérations de formatage des nombres à virgule. La valeur par défaut est le caractère '.', mais en France, le caractère utilisé est la virgule (caractère ','). De même, la méthode thousands_sep permet de déterminer le caractère qui est utilisé pour séparer les groupes de chiffres lors de l'écriture des grands nombres. La valeur par défaut renvoyée par cette fonction est le caractère virgule (caractère ','), mais dans les locales françaises, on utilise généralement un espace (caractère ' '). Enfin, la méthode grouping permet de déterminer les emplacements où ces séparateurs doivent être introduits. La chaîne de caractères renvoyée détermine le nombre de chiffres de chaque groupe de chiffres. Le nombre de chiffres du premier groupe est ainsi stocké dans le premier caractère de la chaîne de caractères renvoyée par la méthode grouping, celui du deuxième groupe est stocké dans le deuxième caractère, et ainsi de suite. Le dernier nombre ainsi obtenu dans cette chaîne de caractères est ensuite utilisé pour tous les groupes de chiffres suivants, ce qui évite d'avoir à définir une chaîne de caractères arbitrairement longue. Un nombre de chiffres nul indique que le mécanisme de groupage des chiffres des grands nombres est désactivé. Les facettes de la plupart des locales renvoient la valeur "\03", ce qui permet de grouper les chiffres par paquets de trois (milliers, millions, milliards, etc.).

    Note : Remarquez que les valeurs stockées dans la chaîne de caractères renvoyée par la méthode grouping sont des valeurs numériques et non des chiffres formatés dans la chaîne de caractères. Ainsi, la valeur par défaut renvoyée est bien "\03" et non "3".

    Les méthodes truename et falsename quant à elles permettent aux facettes de formatage d'obtenir les chaînes de caractères qui représentent les valeurs true et false des booléens. Ce sont ces chaînes de caractères qui sont utilisées lorsque l'option de formatage boolalpha a été activée dans les flux d'entrée / sortie. Les valeurs retournées par ces méthodes sont, par défaut, les mots anglais true et false. Il est concevable dans d'autres locales, cependant, d'avoir des noms différents pour ces deux valeurs. Nous verrons dans la section 16.3.2 la manière de procéder pour redéfinir ces méthodes et construire ainsi une locale personnalisée et francisée.

    Note : Bien entendu, les facettes d'écriture et de lecture des nombres utilisent également les options de formatage qui sont définis au niveau des flux d'entrée / sortie. Pour cela, les opérations d'entrée / sortie reçoivent en paramètre une référence sur le flux contenant ces options.

    16.2.4.2. La facette d'écriture des nombres

    L'écriture et le formatage des nombres sont pris en charge par la facette num_put. Cette facette est déclarée comme suit dans l'en-tête locale :

    template <class charT,
    class OutputIterator = ostreambuf_iterator<charT> >
    class num_put : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef OutputIterator iter_type;

    // Le constructeur :
    explicit num_put(size_t refs = 0);

    // Les méthodes d'écriture des nombres :
    iter_type put(iter_type s, ios_base &f, char_type remplissage, bool v) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage, long v) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage,
    unsigned long v) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage,
    double v) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage,
    long double v) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage,
    void *v) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Comme vous pouvez le constater, cette facette dispose d'une surcharge de la méthode put pour chacun des types de base du langage. Ces surcharges prennent en paramètre un itérateur d'écriture sur le flux de sortie sur lequel les données formatées devront être écrites, une référence sur le flux de sortie contenant les options de formatage à utiliser lors du formatage des nombres, le caractère de remplissage à utiliser, et bien entendu la valeur à écrire.

    En général, ces méthodes sont appelées au sein des opérateurs d'insertion operator<< pour chaque type de donnée existant. De plus, le flux de sortie sur lequel les écritures doivent être effectuées est le même que le flux servant à spécifier les options de formatage, si bien que l'appel aux méthodes put est extrêmement simplifié. Nous verrons plus en détail la manière d'appeler ces méthodes dans la section 16.3.1, lorsque nous écrirons une nouvelle facette pour un nouveau type de donnée.

    16.2.4.3. La facette de lecture des nombres

    Les opérations de lecture des nombres à partir d'un flux de données sont prises en charge par la facette num_get. Cette facette est déclarée comme suit dans l'en-tête locale :

    template <class charT,
    class InputIterator = istreambuf_iterator<charT> >
    class num_get : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef InputIterator iter_type;

    // Le constructeur :
    explicit num_get(size_t refs = 0);

    // Les méthodes de lecture des nombres :
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, bool &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, long &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, unsigned short &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, unsigned int &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, unsigned long &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, float &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, double &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, long double &v) const;
    iter_type get(iter_type in, iter_type end, ios_base &,
    ios_base::iostate &err, void *&v) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Comme vous pouvez le constater, cette facette ressemble beaucoup à la facette num_put. Il existe en effet une surcharge de la méthode get pour chaque type de base du langage. Ces méthodes sont capables d'effectuer la lecture des données de ces types à partir du flux d'entrée, en tenant compte des paramètres des locales et des options de formatage du flux. Ces méthodes prennent en paramètre un itérateur de flux d'entrée, une valeur limite de cet itérateur au-delà de laquelle la lecture du flux ne se fera pas, la référence sur le flux d'entrée contenant les options de formatage et deux paramètres de retour. Le premier paramètre recevra un code d'erreur de type iostate qui pourra être positionné dans le flux d'entrée pour signaler l'erreur. Le deuxième est une référence sur la variable devant accueillir la valeur lue. Si une erreur se produit, cette variable n'est pas modifiée.

    Les méthodes get sont généralement utilisées par les opérateurs d'extraction operator>> des flux d'entrée / sortie pour les types de données de base du langage. En général, ces opérateurs récupèrent la locale incluse dans le flux d'entrée sur lequel ils travaillent et utilisent la facette num_get de cette locale. Ils appellent alors la méthode get permettant de lire la donnée qu'ils doivent extraire, en fournissant ce même flux en paramètre. Ils testent ensuite la variable d'état retournée par la méthode get et, si une erreur s'est produite, modifient l'état du flux d'entrée en conséquence. Cette dernière opération peut bien entendu provoquer le lancement d'une exception, selon le masque d'exceptions utilisé pour le flux.

    16.2.5. Les facettes de gestion des monnaies

    La bibliothèque standard ne définit pas de type de donnée dédiés à la représentation des montants. Elle suppose en effet que les montants sont stockés dans des nombres à virgule flottante dans la plupart des programmes ou, pour les programmes qui désirent s'affranchir des erreurs d'arrondis inévitables lors de l'utilisation de flottants, sous forme textuelle dans des chaînes de caractères. En revanche, la bibliothèque standard fournit, tout comme pour les types standards, deux facettes localisées prenant en compte la lecture et l'écriture des montants. Ces facettes se basent également sur une troisième facette qui regroupe tous les paramètres spécifiques aux conventions nationales.

    Note : En réalité, les seuls types capables de représenter correctement les montants en informatique sont les entiers et les nombres à virgule fixe codés sur les entiers. En effet, les types intégraux sont les seuls types qui ne soulèvent pas de problème de représentation des nombres (à condition qu'il n'y ait pas de débordements bien entendu) et les nombres à virgule fixe sont particulièrement adaptés aux montants, car en général le nombre de chiffres significatifs après la virgule est fixé pour une monnaie donnée. Les nombres à virgule flottante ne permettent pas de représenter des valeurs avec précision et introduisent des erreurs incontrôlables dans les calculs et dans les arrondis. Les chaînes de caractères quant à elles souffrent de leur lourdeur et, dans la plupart des cas, de la nécessité de passer par des nombres à virgule flottantes pour interpréter leur valeur. Les facettes présentées dans cette section sont donc d'une utilité réduite pour les programmes qui cherchent à obtenir des résultats rigoureux et précis, et qui ne tolèrent pas les erreurs de représentation et les erreurs d'arrondis.

    Toutes les facettes de gestion des montants sont des classes template. Cependant, contrairement aux autres facettes, ces facettes disposent d'un autre paramètre template que le type de caractère sur lequel elles travaillent. Ce paramètre est un paramètre de type booléen qui permet, selon sa valeur, de spécifier si les facettes doivent travailler avec la représentation internationale des montants ou non. Il existe en effet une représentation universelle des montants qui, entre autres particularités, utilise les codes internationaux de monnaie (« USD » pour le dollar américain, « CAN » pour le dollar canadien, « EUR » pour l'euro, etc.).

    Comme pour les facettes de gestion des nombres, les facettes prenant en charge les monnaies sont au nombre de trois. Une de ces trois facettes permet d'obtenir des informations sur la monnaie de la locale et les deux autres réalisent respectivement les opérations d'écriture et de lecture sur un flux.

    16.2.5.1. La facette money_punct

    La facette moneypunct est la facette permettant aux deux facettes d'écriture et de lecture des montants d'obtenir les informations relatives à la monnaie de leur locale. Cette facette est déclarée comme suit dans l'en-tête locale :

    template <class charT, bool International = false>
    class moneypunct : public locale::facet, public money_base
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit moneypunct(size_t refs = 0);

    // Les méthodes de lecture des options de formatage des montants :
    charT decimal_point() const;
    charT thousands_sep() const;
    string grouping() const;
    int frac_digits() const;
    string_type curr_symbol() const;
    pattern pos_format() const;
    pattern neg_format() const;
    string_type positive_sign() const;
    string_type negative_sign() const;
    static const bool intl = International;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Comme vous pouvez le constater, cette facette dispose de méthodes permettant de récupérer les divers symboles qui sont utilisés pour écrire les montants de la monnaie qu'elle décrit. Ainsi, la méthode decimal_point renvoie le caractère qui doit être utilisé en tant que séparateur du chiffre des unités de la partie fractionnaire des montants, si celle-ci doit être représentée. De même, la méthode thousands_sep renvoie le caractère qui doit être utilisé pour séparer les groupes de chiffres pour les grands montants, et la méthode grouping renvoie une chaîne contenant, dans chacun de ses caractères, le nombre de chiffres de chaque groupe. Ces méthodes sont donc semblables aux méthodes correspondantes de la facette numpunct. Le nombre de chiffres significatifs après la virgule utilisé pour cette monnaie peut être obtenue grâce à la méthode frac_digits. Ce n'est que si la valeur renvoyée par cette méthode est supérieure à 0 que le symbole de séparation des unités de la partie fractionnaire de la méthode decimal_point est utilisé.

    La méthode curr_symbol permet d'obtenir le symbole monétaire de la monnaie. Ce symbole dépend de la valeur du paramètre template International. Si ce paramètre vaut true, le symbole monétaire renvoyé sera le symbole monétaire international. Dans le cas contraire, ce sera le symbole monétaire en usage dans le pays de circulation de la monnaie. La valeur du paramètre International pourra être obtenu grâce à la constante statique intl de la facette.

    Les méthodes suivantes permettent de spécifier le format d'écriture des montants positifs et négatifs. Ces méthodes utilisent les définitions de constantes et de types de la classe de base money_base dont la facette moneypunct hérite. La classe money_base est déclarée comme suit dans l'en-tête locale :

    class money_base
    {
    public:
    enum part
    {
    none, space, symbol, sign, value
    };
    struct pattern
    {
    char field[4];
    };
    };

    Cette classe contient la définition d'une énumération dont les valeurs permettent d'identifier les différentes composantes d'un montant, ainsi qu'une structure pattern qui contient un tableau de quatre caractères. Chacun de ces caractères peut prendre l'une des valeurs de l'énumération part. La structure pattern définit donc l'ordre dans lequel les composantes d'un montant doivent apparaître. Ce sont des motifs de ce genre qui sont renvoyés par les méthodes pos_format et neg_format, qui permettent d'obtenir respectivement le format des montants positifs et celui des montants négatifs.

    Les différentes valeurs que peuvent prendre les éléments du motif pattern représentent chacune une partie de l'expression d'un montant. La valeur value représente bien entendu la valeur de ce montant, sign son signe et symbol le symbole monétaire. La valeur space permet d'insérer un espace dans l'expression d'un montant, mais les espaces ne peuvent pas être utilisés en début et en fin de montants. Enfin, la valeur none permet de ne rien mettre à la position où il apparaît dans le motif.

    La manière d'écrire les montants positifs et négatifs varie grandement selon les pays. En général, il est courant d'utiliser le signe '-' pour signaler un montant négatif et aucun signe distinctif pour les montants positifs. Cependant, certains pays écrivent les montants négatifs entre parenthèses et la marque des montants négatifs n'est donc plus un simple caractère. Les méthodes positive_sign et negative_sign permettent d'obtenir les symboles à utiliser pour noter les montants positifs et négatifs. Elles retournent toutes les deux une chaîne de caractères, dont le premier est placé systématiquement à l'emplacement auquel la valeur sign a été affectée dans la chaîne de format renvoyée par les méthodes pos_format et neg_format. Les caractères résiduels, s'ils existent, sont placés à la fin de l'expression du montant complètement formatée. Ainsi, dans les locales pour lesquelles les montants négatifs sont écrits entre parenthèses, la chaîne renvoyée par la méthode negative_sign est « () », et pour les locales utilisant simplement le signe négatif, cette chaîne ne contient que le caractère '-'.

    16.2.5.2. Les facettes de lecture et d'écriture des montants

    Les facettes d'écriture et de lecture des montants sont sans doute les facettes standards les plus simples. En effet, elles ne disposent que de méthodes permettant d'écrire et de lire les montants sur les flux. Ces facettes sont respectivement les facettes money_put et money_get. Elles sont définies comme suit dans l'en-tête locale :

    template <class charT, bool Intl = false,
    class OutputIterator = ostreambuf_iterator<charT> >
    class money_put : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef OutputIterator iter_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit money_put(size_t refs = 0);

    // Les méthodes d'écriture des montants :
    iter_type put(iter_type s, bool intl, ios_base &f,
    char_type remplissage, long double units) const;
    iter_type put(iter_type s, bool intl, ios_base &f,
    char_type remplissage, const string_type &digits) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    template <class charT,
    class InputIterator = istreambuf_iterator<charT> >
    class money_get : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef InputIterator iter_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit money_get(size_t refs = 0);

    // Les méthodes de lecture des montants :
    iter_type get(iter_type s, iter_type end, bool intl,
    ios_base &f, ios_base::iostate &err,
    long double &units) const;
    iter_type get(iter_type s, iter_type end, bool intl,
    ios_base &f, ios_base::iostate &err,
    string_type &digits) const;
    static const bool intl = Intl;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Comme vous pouvez le constater, les méthodes d'écriture et de lecture put et get de ces facettes sont semblables aux méthodes correspondantes des facettes de gestion des nombres. Toutefois, elles se distinguent par un paramètre booléen complémentaire qui permet d'indiquer si les opérations de formatage doivent se faire en utilisant les conventions internationales d'écriture des montants. Les autres paramètres ont la même signification que pour les méthodes put et get des facettes de gestion des nombres. En particulier, l'itérateur fourni indique l'emplacement où les données doivent être écrites ou lues, et le flux d'entrée / sortie spécifié permet de récupérer les options de formatage des montants. L'une des options les plus utiles est sans doute l'option qui permet d'afficher la base des nombres, car, dans le cas des facettes de gestion des montants, elle permet d'activer ou non l'écriture du symbole monétaire. Enfin, les méthodes put et get sont fournies en deux exemplaires, un pour chaque type de donnée utilisable pour représenter les montants, à savoir les double et les chaînes de caractères.

    16.2.6. Les facettes de gestion du temps

    La bibliothèque standard ne fournit que deux facettes pour l'écriture et la lecture des dates : la facette time_put et la facette time_get. Ces deux facettes utilisent le type de base struct tm de la bibliothèque C pour représenter le temps. Bien que ce document ne décrive pas les fonctions de la bibliothèque C, il est peut-être bon de rappeler comment les programmes C manipulent les dates en général.

    La gestion du temps dans un programme peut très vite devenir un véritable cauchemar, principalement en raison de la complexité que les êtres humains se sont efforcés de développer dans leur manière de représenter le temps. En effet, il faut tenir compte non seulement des spécificités des calendriers (années bissextiles ou non par exemple), mais aussi des multiples bases de numération utilisées dans l'écriture des dates (24 heures par jour, 60 minutes par heure et 60 secondes par minutes, puis 10 dixièmes dans une seconde et ainsi de suite) et des conventions locales de gestion des heures (fuseau horaire, heure d'été et d'hiver). La règle d'or lors de la manipulation des dates est donc de toujours travailler dans un référentiel unique avec une représentation linéaire du temps, autrement dit, de simplifier tout cela. En pratique, cela revient à dire que les programmes doivent utiliser une représentation linéaire du temps (généralement, le nombre de secondes écoulées depuis une date de référence) et travailler en temps universel. De même, le stockage des dates doit être fait dans ce format afin de garantir la possibilité d'échanger les données sans pour autant laisser la place aux erreurs d'interprétation de ces dates.

    En pratique, la bibliothèque C utilise le type time_t. Les valeurs de ce type représentent le nombre d'instants écoulés depuis le premier janvier 1970 à 0 heure (date considérée comme le début de l'ère informatique par les inventeurs du langage C, Kernighan et Ritchie, et que l'on appelle couramment « Epoch »). La durée de ces instants n'est pas normalisée par la bibliothèque C, mais il s'agit de secondes pour les systèmes POSIX. Le type time_t permet donc de réaliser des calculs simplement sur les dates. Les dates représentées avec des time_t sont toujours exprimées en temps universel.

    Bien entendu, il existe des fonctions permettant de convertir les dates codées sous la forme de time_t en dates humaines et réciproquement. Le type de donnée utilisé pour stocker les dates au format humain est la structure struct tm. Cette structure contient plusieurs champs, qui représentent entre autres l'année, le jour, le mois, les heures, les minutes et les secondes. Ce type contient donc les dates au format éclaté et permet d'obtenir les différentes composantes d'une date.

    Généralement, les dates sont formatées en temps local, car les utilisateurs désirent souvent avoir les dates affichées dans leur propre base de temps. Cependant, il est également possible de formater les dates en temps universel. Ces opérations de formatages sont réalisées par les bibliothèques C et C++, et les programmes n'ont donc pas à se soucier des paramètres de fuseaux horaires, d'heure d'été et d'hiver et des conventions locales d'écriture des dates : tout est pris en charge par les locales.

    Les principales fonctions permettant de manipuler les dates sont récapitulées dans le tableau ci-dessous :

    Tableau 16-1. Fonctions C de gestion des dates

    FonctionDescription
    time_t time(time_t *)

    Permet d'obtenir la date courante. Peut être appelée avec l'adresse d'une variable de type time_t en paramètre ou avec la constante NULL. Initialise la variable passée par pointeur avec la date courante, et renvoie également la valeur écrite.

    struct tm *gmtime(const time_t *)

    Permet de convertir une date stockée dans une variable de type time_t en sa version éclatée en temps universel. Le pointeur renvoyé référence une structure allouée en zone statique par la bibliothèque C et ne doit pas être libéré.

    struct tm *localtime(const time_t *)

    Permet de convertir une date stockée dans une variable de type time_t en sa version éclatée en temps local. Le pointeur renvoyé référence une structure allouée en zone statique par la bibliothèque C et ne doit pas être libéré.

    time_t mktime(struct tm *)

    Permet de construire une date de type time_t à partir d'une date en temps local stockée dans une structure struct tm. Les données membres de la structure struct tm peuvent être corrigées par la fonction mktime si besoin est. Cette fonction est donc la fonction inverse de localtime.

    size_t strftime(char *tampon, size_t max, const char *format, constr struct tm *t)

    Permet de formater une date stockée dans une structure struct tm dans une chaîne de caractères. Cette chaîne doit être fournie en premier paramètre, ainsi que le nombre maximal de caractères que la fonction pourra écrire. La fonction renvoie le nombre de caractères écrits ou, si le premier paramètre est nul, la taille de la chaîne de caractères qu'il faudrait pour effectuer une écriture complète. La fonction strftime prend en paramètre une chaîne de format et fonctionne de manière similaire aux fonctions printf et sprintf. Elle comprend un grand nombre de formats, mais les plus utiles sont sans doute les formats « %X » et « %x », qui permettent respectivement de formater l'heure et la date selon les conventions de la locale du programme.

    Note : Il n'existe pas de fonction permettant de convertir une date exprimée en temps universel et stockée dans une structure struct tm en une date de type time_t. De même, la bibliothèque C ne fournit pas de fonction permettant d'analyser une chaîne de caractères représentant une date. Cependant, la norme Unix 98 définit la fonction strptime, qui est la fonction inverse de la fonction strftime.

    Les fonctions localtime et gmtime ne sont pas sûres dans un environnement multithreadé. En effet, la zone de mémoire renvoyée est en zone statique et est partagée par tous les threads. La bibliothèque C définit donc deux fonctions complémentaires, localtime_r et gmtime_r, qui prennent un paramètre complémentaire qui doit recevoir un pointeur sur la structure struct tm dans lequel le résultat doit être écrit. Cette structure est allouée par le thread appelant et ne risque donc pas d'être détruite par un appel à la même fonction par un autre thread.

    Les facettes de la bibliothèque standard C++ ne permettent pas de manipuler les dates en soi. Elles ne sont destinées qu'à réaliser le formatage des dates en tenant compte des spécificités de représentation des dates de la locale. Elles se comportent exactement comme la fonction strftime le fait lorsque l'on utilise les chaînes de format « %X » et « %x ».

    16.2.6.1. La facette d'écriture des dates

    La facette d'écriture des dates est déclarée comme suit dans l'en-tête locale :

    template <class charT,
    class OutputIterator = ostreambuf_iterator<charT> >
    class time_put : public locale::facet
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef OutputIterator iter_type;

    // Le constructeur :
    explicit time_put(size_t refs = 0);

    // Les méthodes d'écriture des dates :
    iter_type put(iter_type s, ios_base &f, char_type remplissage, const tm *t,
    char format, char modificateur = 0) const;
    iter_type put(iter_type s, ios_base &f, char_type remplissage, const tm *t,
    const charT *debut_format, const charT *fin_format) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Cette facette dispose de deux surcharges de la méthode put permettant d'écrire une date sur un flux de sortie. La première permet d'écrire une date sur le flux de sortie dont un itérateur est donné en premier paramètre. Le formatage de la date se fait comme avec la fonction strftime de la bibliothèque C. Le paramètre modificateur ne doit pas être utilisé en général, sa signification n'étant pas précisée par la norme C++. La deuxième forme de la méthode put réalise également une écriture sur le flux, en prenant comme chaîne de format la première sous-chaîne commençant par le caractère '%' dans la chaîne indiquée par les paramètres debut_format et fin_format.

    16.2.6.2. La facette de lecture des dates

    La facette de lecture des dates permet de lire les dates dans le même format que celui utilisé par la fonction strftime de la bibliothèque C lorsque la chaîne de format vaut « %X » ou « %x ». Cette facette est déclarée comme suit dans l'en-tête locale :

    template <class charT,
    class InputIterator = istreambuf_iterator<charT> >
    class time_get : public locale::facet, public time_base
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef InputIterator iter_type;

    // Le constructeur :
    explicit time_get(size_t refs = 0);

    // Les méthodes de gestion de la lecture des dates :
    iter_type get_time(iter_type s, iter_type end, ios_base &f,
    ios_base::iostate &err, tm *t) const;
    iter_type get_date(iter_type s, iter_type end, ios_base &f,
    ios_base::iostate &err, tm *t) const;
    iter_type get_weekday(iter_type s, iter_type end, ios_base &f,
    ios_base::iostate &err, tm *t) const;
    iter_type get_monthname(iter_type s, iter_type end, ios_base &f,
    ios_base::iostate &err, tm *t) const;
    iter_type get_year(iter_type s, iter_type end, ios_base &f,
    ios_base::iostate &err, tm *t) const;
    dateorder date_order() const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Les différentes méthodes de cette facette permettent respectivement d'obtenir l'heure, la date, le jour de la semaine, le nom du mois et l'année d'une date dans le flux d'entrée spécifié par l'itérateur fourni en premier paramètre. Toutes ces données sont interprétées en fonction de la locale à laquelle la facette appartient.

    Enfin, la méthode date_order permet d'obtenir l'une des valeurs de l'énumération définie dans la classe de base time_base et qui indique l'ordre dans lequel les composants jour / mois / année des dates apparaissent dans la locale de la facette. La classe de base time_base est déclarée comme suit dans l'en-tête locale :

    class time_base
    {
    public:
    enum dateorder
    {
    no_order, dmy, mdy, ymd, ydm
    };
    };

    La signification des différentes valeurs de l'énumération est immédiate. La seule valeur nécessitant des explications complémentaires est la valeur no_order. Cette valeur est renvoyée par la méthode date_order si le format de date utilisé par la locale de la facette contient d'autres champs que le jour, le mois et l'année.

    Note : La méthode date_order est fournie uniquement à titre de facilité par la bibliothèque standard. Elle peut ne pas être implémentée pour certaines locales. Dans ce cas, elle renvoie systématiquement la valeur no_order.

    16.2.7. Les facettes de gestion des messages

    Afin de faciliter l'internationalisation des programmes, la bibliothèque standard fournit la facette messages, qui permet de prendre en charge la traduction de tous les messages d'un programme de manière indépendante du système sous-jacent. Cette facette permet d'externaliser tous les messages des programmes dans des fichiers de messages que l'on appelle des catalogues. Le format et l'emplacement de ces fichiers ne sont pas spécifiés par la norme C++, cependant, la manière d'y accéder est standardisée et permet d'écrire des programmes portables. Ainsi, lorsqu'un programme devra être traduit, il suffira de traduire les messages stockés dans les fichiers de catalogue pour chaque langue et de les distribuer avec le programme.

    Note : La manière de créer et d'installer ces fichiers étant spécifique à chaque implémentation de la bibliothèque standard et, dans une large mesure, spécifique au système d'exploitation utilisé, ces fichiers ne seront pas décrits ici. Seule la manière d'utiliser la facette messages sera donc indiquée. Reportez-vous à la documentation de votre environnement de développement pour plus de détails sur les outils permettant de générer les fichiers de catalogue.

    La facette messages référence les fichiers de catalogue à l'aide d'un type de donnée spécifique. Ce type de donnée est défini dans la classe de base messages_base comme étant un type intégral :

    class messages_base
    {
    public:
    typedef int catalog;
    };
    La classe template messages de gestion de la facette hérite donc de cette classe de base et utilise le type catalog pour identifier les fichiers de catalogue de l'application.

    La classe messages est déclarée comme suit dans l'en-tête locale :

    template <class charT>
    class messages : public locale::facet, public messages_base
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef basic_string<charT> string_type;

    // Le constructeur :
    explicit messages(size_t refs = 0);

    // Les méthodes de gestion des catalogues de messages :
    catalog open(const basic_string<char> &nom, const locale &l) const;
    void close(catalog c) const;
    string_type get(catalog c, int groupe, int msg,
    const string_type &defaut) const;

    // L'identificateur de la facette :
    static locale::id id;
    };

    Note : Les méthodes virtuelles d'implémentation des méthodes publiques n'ont pas été écrites dans la déclaration précédente par souci de simplification. Elles existent malgré tout, et peuvent être redéfinies par les classes dérivées afin de personnaliser le comportement de la facette.

    Les principales méthodes de gestion des catalogues sont les méthodes open et close. Comme leurs noms l'indiquent, ces méthodes permettent d'ouvrir un nouveau fichier de catalogue et de le fermer pour en libérer les ressources. La méthode open prend en paramètre le nom du catalogue à ouvrir. Ce nom doit identifier de manière unique le catalogue, mais la norme C++ n'indique pas comment il doit être interprété. Cela relève donc de l'implémentation de la bibliothèque standard utilisée. Toutefois, en pratique, il est probable qu'il s'agit d'un nom de fichier. Le deuxième paramètre permet d'indiquer la locale à utiliser pour effectuer les conversions de jeux de caractères si cela est nécessaire. Il permet donc de laisser au programmeur le choix du jeu de caractères dans lequel les messages seront écrits dans le catalogue. La valeur renvoyée par la méthode open est l'identifiant du catalogue, identifiant qui devra être fourni à la méthode get pour récupérer les messages du catalogue et à la méthode close pour fermer le fichier de catalogue. Si l'ouverture du fichier n'a pas pu être effectuée, la méthode open renvoie une valeur inférieure à 0.

    Les messages du catalogue peuvent être récupérés à l'aide de la méthode get. Cette méthode prend en paramètre l'identifiant d'un catalogue précédemment obtenu par l'intermédiaire de la méthode open, un identifiant de groupe de message et un identifiant d'un message. Le dernier paramètre doit recevoir la valeur par défaut du message en cas d'échec de la recherche du message dans le catalogue. Cette valeur par défaut est souvent un message en anglais, ce qui permet au programme de fonctionner correctement même lorsque ses fichiers de catalogue sont vides.

    La manière dont les messages sont identifiés n'est pas spécifiée par la norme C++, tout comme la manière dont ils sont classés en groupes de messages au sein d'un même fichier de catalogue. Cela relève donc de l'implémentation de la bibliothèque utilisée. Consultez la documentation de votre environnement de développement pour plus de détails à ce sujet.

    Note : Cette facette est relativement peu utilisée, pour plusieurs raison. Premièrement, peu d'environnements C++ respectent la norme C++ à ce jour. Deuxièmement, les systèmes d'exploitation disposent souvent de mécanismes de localisation performants et pratiques. Enfin, l'identification d'un message par des valeurs numériques n'est pas toujours pratique et il est courant d'utiliser le message par défaut, souvent en anglais, comme clef de recherche pour les messages internationaux. Cette manière de procéder est en effet beaucoup plus simple, puisque le contenu des messages est écrit en clair dans la langue par défaut dans les fichiers sources du programme.


    votre commentaire
  • Chapitre 16. Les locales


    Il existe de nombreux alphabets et de nombreuses manières d'écrire les nombres, les dates et les montants de part le monde. Chaque pays, chaque culture dispose en effet de ses propres conventions et de ses propres règles, et ce dans de nombreux domaines. Par exemple, les Anglo-saxons ont pour coutume d'utiliser le point (caractère '.') pour séparer les unités de la virgule lorsqu'ils écrivent des nombres à virgule et d'utiliser une virgule (caractère ',') entre chaque groupe de trois chiffres pour séparer les milliers des millions, les millions des milliards, etc. En France, c'est la virgule est utilisée pour séparer les unités de la partie fractionnaire des nombres à virgule, et le séparateur des milliers est simplement un espace. De même, ils ont l'habitude d'écrire les dates en mettant le mois avant les jours, alors que les Français font l'inverse.

    Il va de soi que ce genre de différences rend techniquement très difficile l'internationalisation des programmes. Une solution est tout simplement de dire que les programmes travaillent dans une langue « neutre », ce qui en pratique revient souvent à dire l'anglais puisque c'est la langue historiquement la plus utilisée en informatique. Hélas, si cela convenait parfaitement aux programmeurs, ce ne serait certainement pas le cas des utilisateurs ! Il faut donc, à un moment donné ou à un autre, que les programmes prennent en compte les conventions locales de chaque pays ou de chaque peuple.

    Ces conventions sont extrêmement nombreuses et portent sur des domaines aussi divers et variés que la manière d'écrire les nombres, les dates ou de coder les caractères et de classer les mots dans un dictionnaire. En informatique, il est courant d'appeler l'ensemble des conventions d'un pays la locale de ce pays. Les programmes qui prennent en compte la locale sont donc dits localisés et sont capables de s'adapter aux préférences nationales de l'utilisateur.

    Note : Le fait d'être localisé ne signifie pas pour autant pour un programme que tous ses messages sont traduits dans la langue de l'utilisateur. La localisation ne prend en compte que les aspects concernant l'écriture des nombres et les alphabets utilisés. Afin de bien faire cette distinction, on dit que les programmes capables de communiquer avec l'utilisateur dans sa langue sont internationalisés. La conversion d'un programme d'un pays à un autre nécessite donc à la fois la localisation de ce programme et son internationalisation.

    Si la traduction de tous les messages d'un programme ne peut pas être réalisée automatiquement, il est toutefois possible de prendre en compte les locales relativement facilement. En effet, les fonctionnalités des bibliothèques C et C++, en particulier les fonctionnalités d'entrée / sortie, peuvent généralement être paramétrées par la locale de l'utilisateur. La gestion des locales est donc complètement prise en charge par ces bibliothèques et un même programme peut donc être utilisé sans modification dans divers pays.

    Note : En revanche, la traduction des messages ne peut bien évidemment pas être prise en charge par la bibliothèque standard, sauf éventuellement pour les messages d'erreur du système. La réalisation d'un programme international nécessite donc de prendre des mesures particulières pour faciliter la traduction de ces messages. En général, ces mesures consistent à isoler les messages dans des modules spécifiques et à ne pas les utiliser directement dans le code du programme. Ainsi, il suffit simplement de traduire les messages de ces modules pour ajouter le support d'une nouvelle langue à un programme existant. Le code source n'a ainsi pas à être touché, ce qui limite les risques d'erreurs.

    La gestion des locales en C++ se fait par l'intermédiaire d'une classe générale, la classe locale, qui permet de stocker tous les paramètres locaux des pays. Cette classe est bien entendu utilisée par les flux d'entrée / sortie de la bibliothèque standard, ce qui fait que vous n'aurez généralement qu'à initialiser cette classe au début de vos programmes pour leur faire prendre en compte les locales. Cependant, il se peut que vous ayez à manipuler vous-même des locales ou à définir de nouvelles conventions nationales, surtout si vous écrivez des surcharges des opérateurs de formatage des flux operator<< et operator>>. Ce chapitre présente donc les notions générales des locales, les différentes classes mises en oeuvre au sein d'une même locale pour prendre en charge tous les aspects de la localisation, et la manière de définir ou de redéfinir un de ces aspects afin de compléter une locale existante.

    16.1. Notions de base et principe de fonctionnement des facettes

    Comme il l'a été dit plus haut, les locales comprennent différents aspects qui traitent chacun d'une des conventions nationales de la locale. Par exemple, la manière d'écrire les nombres constitue un de ces aspects, tout comme la manière de classer les caractères ou la manière d'écrire les heures et les dates.

    Chacun de ces aspects constitue ce que la bibliothèque standard C++ appelle une facette. Les facettes sont gérées par des classes C++, dont les méthodes permettent d'obtenir les informations spécifiques aux données qu'elles manipulent. Certaines facettes fournissent également des fonctions permettant de formater et d'interpréter ces données en tenant compte des conventions de leur locale. Chaque facette est identifiée de manière unique dans le programme, et chaque locale contient une collection de facettes décrivant tous ses aspects.

    La bibliothèque standard C++ fournit bien entendu un certain nombre de facettes prédéfinies. Ces facettes sont regroupées en catégories qui permettent de les classer en fonction du type des opérations qu'elles permettent de réaliser. La bibliothèque standard définit six catégories de facettes, auxquelles correspondent les valeurs de six constantes de la classe locale :

    • la catégorie ctype, qui regroupe toutes les facettes permettant de classifier les caractères et de les convertir d'un jeu de caractère en un autre ;

    • la catégorie collate, qui comprend une unique facette permettant de comparer les chaînes de caractères en tenant compte des caractères de la locale courante et de la manière de les utiliser dans les classements alphabétiques ;

    • la catégorie numeric, qui comprend toutes les facettes prenant en charge le formatage des nombres ;

    • la catégorie monetary, qui comprend les facettes permettant de déterminer les symboles monétaires et la manière d'écrire les montants ;

    • la catégorie time, qui comprend les facettes capables d'effectuer le formatage et l'écriture des dates et des heures ;

    • la catégorie message, qui contient une unique facette permettant de faciliter l'internationalisation des programmes en traduisant les messages destinés aux utilisateurs.

    Bien entendu, il est possible de définir de nouvelles facettes et de les inclure dans une locale existante. Ces facettes ne seront évidemment pas utilisées par les fonctions de la bibliothèque standard, mais le programme peut les récupérer et les utiliser de la même manière que les facettes standards.

    Les mécanismes de définition, d'identification et de récupération des facettes, sont tous pris en charge au niveau de la classe locale. La déclaration de cette classe est réalisée de la manière suivante dans l'en-tête locale :

    class locale
    {
    public:
    // Les types de données :
    // Les catégories de facettes :
    typedef int category;
    static const category // Les valeurs de ces constantes
    { // sont spécifiques à chaque implémentation
    none = VAL0,
    ctype = VAL1, collate = VAL2,
    numeric = VAL3, monetary = VAL4,
    time = VAL5, messages = VAL6,
    all = collate | ctype | monetary | numeric | time | messages;
    };

    // La classe de base des facettes :
    class facet
    {
    protected:
    explicit facet(size_t refs = 0);
    virtual ~facet();
    private:
    // Ces méthodes sont déclarées mais non définies :
    facet(const facet &);
    void operator=(const facet &);
    };

    // La classe d'identification des facettes :
    class id
    {
    public:
    id();
    private:
    // Ces méthodes sont déclarées mais non définies :
    void id(const id &);
    void operator=(const id &);
    };

    // Les constructeurs :
    locale() throw()
    explicit locale(const char *nom);
    locale(const locale &) throw();
    locale(const locale &init, const char *nom, category c);
    locale(const locale &init1, const locale &init2, category c);

    template <class Facet>
    locale(const locale &init, Facet *f);

    template <class Facet>
    locale(const locale &init1, const locale &init2);

    // Le destructeur :
    ~locale() throw();

    // Opérateur d'affectation :
    const locale &operator=(const locale &source) throw();

    // Les méthodes de manipulation des locales :
    basic_string<char> name() const;
    bool operator==(const locale &) const;
    bool operator!=(const locale &) const;
    template <class charT, class Traits, class Allocator>
    bool operator()(
    const basic_string<charT, Traits, Allocator> &s1,
    const basic_string<charT, Traits, Allocator> &s2) const;

    // Les méthodes de sélection des locales :
    static locale global(const locale &);
    static const locale &classic();
    };

    Comme vous pouvez le constater, outre les constructeurs, destructeur et méthodes générales, la classe locale contient la déclaration de deux sous-classes utilisées pour la définition des facettes : la classe facet et la classe id.

    La classe facet est la classe de base de toutes les facettes. Son rôle est essentiellement d'éviter que l'on puisse dupliquer une facette ou en copier une, ce qui est réalisé en déclarant en zone privée le constructeur de copie et l'opérateur d'affectation. Comme ces méthodes ne doivent pas être utilisées, elles ne sont pas définies non plus, seule la déclaration est fournie par la bibliothèque standard. Notez que cela n'est pas dérangeant pour l'utilisation de la classe facet, puisque comme ces méthodes ne sont pas virtuelles et qu'elles ne seront jamais utilisées dans les programmes, l'éditeur de liens ne cherchera pas à les localiser dans les fichiers objets du programme. Ainsi, les instances de facettes ne peuvent être ni copiées, ni dupliquées. En fait, les facettes sont destinées à être utilisées au sein des locales, qui prennent en charge la gestion de leur durée de vie. Toutefois, si l'on désire gérer soi-même la durée de vie d'une facette, il est possible de le signaler lors de la construction de la facette. Le constructeur de base de la classe facet prend en effet un paramètre unique qui indique si la durée de vie de la facette doit être prise en charge par la locale dans laquelle elle se trouve ou si elle devra être explicitement détruite par le programmeur (auquel cas ce paramètre doit être fixé à 1). En général, la valeur par défaut 0 convient dans la majorité des cas et il n'est pas nécessaire de fournir de paramètre au constructeur des facettes.

    La classe id quant à elle est utilisée pour définir des identifiants uniques pour chaque classe de facette. Ces identifiants permettent à la bibliothèque standard de distinguer les facettes les unes des autres, à l'aide d'une clef unique dont le format n'est pas spécifié, mais qui est déterminée par la classe id automatiquement lors de la première création de chaque facette. Cet identifiant est en particulier utilisé pour retrouver les facettes dans la collection des facettes gérées par la classe locale.

    Pour que ce mécanisme d'enregistrement fonctionne, il faut que chaque classe de facette définisse une donnée membre statique id en zone publique, dont le type est la sous-classe id de la classe locale. Cette donnée membre étant statique, elle appartient à la classe et non à chaque instance, et permet donc bien d'identifier chaque classe de facette. Lors du chargement du programme, les variables statiques sont initialisées à 0, ce qui fait que les facettes disposent toutes d'un identifiant nul. Ceci permet aux méthodes de la bibliothèque standard de déterminer si un identifiant a déjà été attribué à la classe d'une facette ou non lorsqu'elle est utilisée pour la première fois. Si c'est le cas, cet identifiant est utilisé tel quel pour référencer cette classe, sinon, il est généré automatiquement et stocké dans la donnée membre id de la classe.

    Ainsi, pour définir une facette, il suffit simplement d'écrire une classe dérivant de la classe locale::facet et contenant une donnée membre statique et publique id de type locale::id. Dès lors, cette facette se verra attribuer automatiquement un identifiant unique, qui permettra d'utiliser les fonctions de recherche de facettes dans les locales. Ces fonctions utilisent bien entendu la donnée membre id du type de la facette à rechercher et s'en servent en tant qu'index dans la collection des facettes de la locale à utiliser. La manière de réaliser ces opérations n'est pas décrite par la norme du C++, mais le principe est là.

    Les fonctions de recherche des facettes sont également déclarées dans l'en-tête locale. Ce sont des fonctions template paramétrées par le type des facettes, qui prennent en paramètre la locale dans laquelle la facette doit être recherchée :

    template <class Facet>
    const Facet &use_facet(const locale &);

    template <class Facet>
    bool has_facet(const locale &);

    La fonction use_facet permet de récupérer l'instance d'une facette dans la locale passée en paramètre. Comme la signature de cette fonction template ne permet pas de déterminer le type de la facette, et donc l'instance à utiliser pour l'appel, il est nécessaire de spécifier explicitement ce type entre crochets après le nom de la fonction. Par exemple, pour récupérer la facette num_put<char> de la locale classique de la bibliothèque C, on fera l'appel suivant :

    use_facet<num_put<char> >(locale::classic());

    Les facettes fournies par la bibliothèque standard sont généralement disponibles et peuvent être utilisées avec les locales. Les méthodes spécifiques à chacune de ces facettes peuvent donc être appelées sur la référence de la facette retournée par la fonction use_facet. En revanche, les facettes définies par l'utilisateur peuvent ne pas être présentes dans la locale fournie en paramètre à la fonction use_facet. Dans ce cas, cette fonction lance une exception de type bad_cast. Comme il peut être utile en certaines circonstances de déterminer dynamiquement si une locale contient ou non une facette, la bibliothèque standard met à disposition la fonction globale has_facet. Cette fonction s'utilise de la même manière que la fonction use_facet, mais au lieu de renvoyer la facette demandée, elle retourne un booléen indiquant si la locale fournie en paramètre contient ou non cette facette.

    Les programmes peuvent créer plusieurs locales afin de prendre en compte plusieurs jeux de paramètres internationaux s'ils le désirent, mais ils doivent dans ce cas manipuler ces locales eux-mêmes dans toutes les opérations susceptibles d'utiliser la notion de locale. Par exemple, ils doivent spécifier la locale à utiliser avant chaque opération d'entrée / sortie en appelant la méthode imbue des flux utilisés. Comme cela n'est pas très pratique, la bibliothèque standard définit une locale globale, qui est la locale utilisée par défaut lorsqu'un programme ne désire pas spécifier explicitement la locale à utiliser. Cette locale peut être récupérée à tout moment en créant un simple objet de type locale, en utilisant le constructeur par défaut de la classe locale. Ce constructeur initialise en effet la locale en cours de construction avec tous les paramètres de la locale globale. Ainsi, pour récupérer la facette num_put<char> de la locale globale, on fera l'appel suivant :

    use_facet<num_put<char> >(locale());
    Vous remarquerez que la locale fournie en paramètre à la fonction use_facet n'est plus, contrairement à l'exemple précédent, la locale renvoyée par la méthode statique classic de la classe locale, mais une copie de la locale globale.

    Il est possible de construire une locale spécifique explicitement avec le constructeur de la classe locale qui prend le nom de la locale à utiliser en paramètre. Ce nom peut être l'un des noms standards "C", "", ou toute autre valeur dont la signification n'est pas normalisée. Le nom de locale vide ("") permet de construire une locale dont les paramètres sont initialisés en fonction de l'environnement d'exécution du programme. C'est donc la valeur que l'on utilisera en général, car cela permet de paramétrer le comportement des programmes facilement, sans avoir à les modifier et à les recompiler.

    Note : La manière de définir la locale dans l'environnement d'exécution des programmes est spécifique à chaque système d'exploitation et n'est normalisé ni par la norme C++, ni par la norme C. La norme POSIX précise cependant que cela peut être réalisé par l'intermédiaire de variables d'environnement. Par exemple, si la variable d'environnement LANG n'est pas définie, la locale utilisée sera la locale de la bibliothèque C. En revanche, si cette variable d'environnement contient la valeur "fr_FR", la locale utilisée sera celle des francophones de France. Les formats des nombres, des dates, etc. utilisés seront donc ceux qui sont en vigueur en France.

    Les autres constructeurs de la classe locale permettent de créer de nouvelles locales en recopiant les facettes d'une locale existante, éventuellement en ajoutant de nouvelles facettes non standards ou en redéfinissant certaines facettes de la locale modèle. Bien entendu, le constructeur de copie recopie toutes les facettes de la locale source dans la locale en cours de construction. Les deux constructeurs suivants permettent de recopier toutes les facettes d'une locale, sauf les facettes identifiées par la catégorie spécifiée en troisième paramètre. Pour cette catégorie, les facettes utilisées sont celles de la locale spécifiée en deuxième paramètre. Il est possible ici d'identifier cette deuxième locale soit par son nom, soit directement par l'intermédiaire d'une référence. Enfin, les deux constructeurs template permettent de créer une locale dont toutes les facettes sont initialisées à partir d'une locale fournie en paramètre, sauf la facette dont le type est utilisé en tant que paramètre template. Pour cette facette, la valeur à utiliser peut être spécifiée directement en deuxième paramètre ou extraite d'une autre locale, elle aussi spécifiée en deuxième paramètre. Nous verrons plus en détail dans la section 16.3 la manière de procéder pour insérer une nouvelle facette ou remplacer une facette existante.

    Enfin, la classe locale dispose de deux méthodes statiques permettant de manipuler les locales du programme. La méthode classic que l'on a utilisée dans l'un des exemples précédents permet d'obtenir la locale représentant les options de la bibliothèque C standard. Cette locale est la locale utilisée par défaut par les programmes qui ne définissent pas les locales utilisées. La méthode global quant à elle permet de spécifier la locale globale du programme. Cette méthode prend en paramètre un objet de type locale à partir duquel la locale globale est initialisée. Il est donc courant de faire un appel à cette méthode dès le début des programmes C++.

    Exemple 16-1. Programme C++ prenant en compte la locale de l'environnement

    #include <ctime>
    #include <iostream>
    #include <locale>

    using namespace std;

    int main(void)
    {
    // Utilise la locale définie dans l'environnement
    // d'exécution du programme :
    locale::global(locale(""));
    // Affiche la date courante :
    time_t date;
    time(&date);
    struct tm *TL = localtime(&date);
    use_facet<time_put<char> >(locale()).put(
    cout, cout, ' ', TL, 'c');
    cout << endl;
    // Affiche la date avec la fonction strftime
    // de la bibliothèque C :
    char s[64];
    strftime(s, 64, "%c", TL);
    cout << s << endl;
    return 0;
    }

    La méthode global de la classe global appelle automatiquement la méthode setlocale si la locale fournie en paramètre a un nom. Cela signifie que les locales de la bibliothèque standard C++ et celles de la bibliothèque standard C sont compatibles et utilisent les mêmes conventions de nommage. En particulier, les programmes qui veulent utiliser la locale définie dans leur environnement d'exécution peuvent utiliser la locale anonyme "". C'est ce que fait le programme de l'exemple précédent, qui affiche la date au format de la locale définie par l'utilisateur en passant par les mécanismes du C++ (via la facette time_put, qui sera décrite en détail dans la section 16.6) et par la fonction strftime de la bibliothèque C.

    Note : Les fonctions time, localtime et strftime sont des fonctions de la bibliothèque C standard. Elles permettent respectivement d'obtenir une valeur de type time_t représentant la date courante, de la convertir en une structure contenant les différentes composantes de la date en temps local, et de formater cette date selon les conventions de la locale courante. Ces fonctions ne seront pas décrites plus en détail ici. Vous pouvez consulter la bibliographie si vous désirez obtenir plus de détails sur la bibliothèque C et les fonctions qu'elle contient.



    votre commentaire
  • 15.6. Les flux d'entrée / sortie sur fichiers

    Les classes d'entrée / sortie sur les fichiers sont implémentées de manière similaire aux classes d'entrée / sortie sur les chaînes de caractères, à ceci près que leurs méthodes spécifiques permettent de manipuler un fichier au lieu d'une chaîne de caractères. Ainsi, la classe basic_ofstream dérive de basic_ostream, la classe basic_ifstream de la classe basic_istream, et la classe basic_fstream de la classe basic_iostream. Toutes ces classes sont déclarées dans l'en-tête fstream. Vous trouverez à titre d'exemple la déclaration de la classe basic_ofstream tel qu'il apparaît dans cet en-tête :

    template <class charT,
    class traits = char_traits<charT> >
    class basic_ofstream : public basic_ostream<charT, traits>
    {
    public:
    // Les types de données :
    typedef charT char_type;
    typedef typename traits::int_type int_type;
    typedef typename traits::pos_type pos_type;
    typedef typename traits::off_type off_type;

    // Les constructeurs et destructeurs :
    basic_ofstream();
    explicit basic_ofstream(const char *nom,
    ios_base::openmode mode = ios_base::out | ios_base::trunc);

    // Les méthodes de gestion du fichier :
    basic_filebuf<charT, traits> *rdbuf() const;
    bool is_open();
    void open(const char *nom, ios_base::openmode mode = out | trunc);
    void close();
    };

    Comme pour les flux d'entrée / sortie sur les chaînes de caractères, il est possible d'initialiser le flux dès sa construction ou a posteriori. Les méthodes importantes sont bien entendu la méthode open, qui permet d'ouvrir un fichier, la méthode is_open, qui permet de savoir si le flux contient déjà un fichier ouvert ou non, et la méthode close, qui permet de fermer le fichier ouvert.

    Exemple 15-10. Utilisation de flux d'entrée / sortie sur un fichier

    #include <iostream>
    #include <fstream>
    #include <string>

    using namespace std;

    int main(void)
    {
    // Lit les données :
    int i;
    double d, e;
    cout << "Entier Réel Réel : ";
    cin >> i >> d >> e;
    // Enregistre ces données dans un fichier :
    ofstream f("fichier.txt");
    if (f.is_open())
    {
    f << "Les données lues sont : " <<
    i << " " << d << " " << e << endl;
    f.close();
    }
    return 0;
    }

    Note : Il est important de bien noter que le destructeur des flux ne ferme pas les fichiers. En effet, ce sont les tampons utilisés de manière sous-jacente par les flux qui réalisent les opérations sur les fichiers. Il est même tout à fait possible d'accéder à un même fichier avec plusieurs classes de flux, bien que cela ne soit pas franchement recommandé. Vous devrez donc prendre en charge vous-même les opérations d'ouverture et de fermeture des fichiers.

    Bien entendu, les classes de flux permettant d'accéder à des fichiers héritent des méthodes de positionnement de leurs classes de base. Ainsi, les classes de lecture dans un fichier disposent des méthodes tellg et seekg, et les classes d'écriture disposent des méthodes tellp et seekp. Ces opérations permettent respectivement de lire et de fixer une nouvelle valeur du pointeur de position du fichier courant.

    Exemple 15-11. Repositionnement du pointeur de fichier dans un flux d'entrée / sortie

    #include <iostream>
    #include <fstream>
    #include <string>

    using namespace std;

    int main(void)
    {
    // Ouvre le fichier de données :
    fstream f("fichier.txt",
    ios_base::in | ios_base::out | ios_base::trunc);
    if (f.is_open())
    {
    // Écrit les données :
    f << 2 << " " << 45.32 << " " << 6.37 << endl;
    // Replace le pointeur de fichier au début :
    f.seekg(0);
    // Lit les données :
    int i;
    double d, e;
    f >> i >> d >> e;
    cout << "Les données lues sont : " <<
    i << " " << d << " " << e << endl;
    // Ferme le fichier :
    f.close();
    }
    return 0;
    }

    Note : Les classes d'entrée / sortie sur fichier n'utilisent qu'un seul tampon pour accéder aux fichiers. Par conséquent, il n'existe qu'une seule position dans le fichier, qui sert à la fois à la lecture et à l'écriture.


    votre commentaire