• II. La bibliothèque standard C++

    Tout comme pour le langage C, pour lequel un certain nombre de fonctions ont été définies et standardisées et constituent la bibliothèque C, une bibliothèque de classes et de fonctions a été spécifiée pour le langage C++. Cette bibliothèque est le résultat de l'évolution de plusieurs bibliothèques, parfois développées indépendamment par plusieurs fournisseurs d'environnements C++, qui ont été fusionnées et normalisées afin de garantir la portabilité des programmes qui les utilisent. Une des principales briques de cette bibliothèque est sans aucun doute la STL (abréviation de « Standard Template Library »), à tel point qu'il y a souvent confusion entre les deux.

    Cette partie a pour but de présenter les principales fonctionnalités de la bibliothèque standard C++. Bien entendu, il est hors de question de décrire complètement chaque fonction ou chaque détail du fonctionnement de la bibliothèque standard, car cela rendrait illisibles et incompréhensibles les explications. Cependant, les informations de base vous seront données afin de vous permettre d'utiliser efficacement la bibliothèque standard C++ et de comprendre les fonctionnalités les plus avancées lorsque vous vous y intéresserez.

    La bibliothèque standard C++ est réellement un sujet de taille. À titre indicatif, sa description est aussi volumineuse que celle du langage lui-même dans la norme C++. Mais ce n'est pas tout, il faut impérativement avoir compris en profondeur les fonctionnalités les plus avancées du C++ pour appréhender correctement la bibliothèque standard. En particulier, tous les algorithmes et toutes les classes fournies par la bibliothèque sont susceptibles de travailler sur des données de type arbitraire. La bibliothèque utilise donc complètement la notion de template, et se base sur plusieurs abstractions des données manipulées et de leurs types afin de rendre générique l'implémentation des fonctionnalités. De plus, la bibliothèque utilise le mécanisme des exceptions afin de signaler les erreurs qui peuvent se produire lors de l'exécution des méthodes de ses classes et de ses fonctions. Enfin, un certain nombre de notions algorithmiques avancées sont utilisées dans toute la bibliothèque. La présentation qui sera faite sera donc progressive, tout en essayant de conserver un ordre logique. Tout comme pour la partie précédente, il est probable que plusieurs lectures seront nécessaires aux débutants pour assimiler toutes les subtilités de la bibliothèque.

    Le premier chapitre de cette partie (chapitre 13) présente les notions de base qui sont utilisées dans toute la libraire : encapsulation des fonctions de la bibliothèque C classique, classes de traits pour les types de base, notion d'itérateurs, de foncteurs, d'allocateurs mémoire et de complexité algorithmique. Le  chapitre 14 présente les types complémentaires que la bibliothèque standard C++ définit pour faciliter la vie du programmeur. Le plus important de ces types est sans doute la classe de gestion des chaînes de caractères basic_string. Le chapitre 15 présente les notions de flux d'entrée / sortie standards, et la notion de tampon pour ces flux. Les mécanismes de localisation (c'est-à-dire les fonctions de paramétrage du programme en fonction des conventions et des préférences nationales) seront décrits dans le chapitre 16. Le chapitre 17est sans doute l'un des plus importants, puisqu'il présente tous les conteneurs fournis par la bibliothèque standard. Enfin, le chapitre 18 décrit les principaux algorithmes de la bibliothèque, qui permettent de manipuler les données stockées dans les conteneurs.

    Les informations décrites ici sont basées sur la norme ISO 14882 du langage C++, et non sur la réalité des environnements C++ actuels. Il est donc fortement probable que bon nombre d'exemples fournis ici ne soient pas utilisables tels quels sur les environnements de développement existants sur le marché, bien que l'on commence à voir apparaître des environnements presque totalement respectueux de la norme maintenant. De légères différences dans l'interface des classes décrites peuvent également apparaître et nécessiter la modification de ces exemples. Cependant, à terme, tous les environnements de développement respecteront les interfaces spécifiées par la norme, et les programmes utilisant la bibliothèque standard seront réellement portables au niveau source.


    votre commentaire
  • 12.7. Fonctions exportées

    Comme on l'a vu, les fonctions et classes template sont toutes instanciées lorsqu'elles sont rencontrées pour la première fois par le compilateur ou lorsque la liste de leurs paramètres est fournie explicitement.

    Cette règle a une conséquence majeure : la définition complète des fonctions et des classes template doit être incluse dans chacun des fichiers dans lequel elles sont utilisées. En général, les déclarations et les définitions des fonctions et des classes template sont donc regroupées ensemble dans les fichiers d'en-tête (et le code ne se trouve pas dans un fichier C++). Cela est à la fois très lent (la définition doit être relue par le compilateur à chaque fois qu'un template est utilisé) et ne permet pas de protéger le savoir faire des entreprises qui éditent des bibliothèques template, puisque leur code est accessible à tout le monde.

    Afin de résoudre ces problèmes, le C++ permet de « compiler » les fonctions et les classes template, et ainsi d'éviter l'inclusion systématique de leur définition dans les fichiers sources. Cette « compilation » se fait à l'aide du mot clé export.

    Pour parvenir à ce résultat, vous devez déclarer « export » les fonctions et les classes template concernées. La déclaration d'une classe template export revient à déclarer export toutes ses fonctions membres non inline, toutes ses données statiques, toutes ses classes membres et toutes ses fonctions membres template non statiques. Si une fonction template est déclarée comme étant inline, elle ne peut pas être de type export.

    Les fonctions et les classes template qui sont définies dans un espace de nommage anonyme ne peuvent pas être déclarées export. Voir le chapitre 11 plus de détails sur les espaces de nommage.

    Exemple 12-16. Mot-clé export

    export template <class T>
    void f(T); // Fonction dont le code n'est pas fourni
    // dans les fichiers qui l'utilisent.

    Dans cet exemple, la fonction f est déclarée export. Sa définition est fournie dans un autre fichier, et n'a pas besoin d'être fournie pour que f soit utilisable.

    Les définitions des fonctions et des classes déclarées export doivent elles aussi utiliser le mot clé export. Ainsi, la définition de f pourra ressembler aux lignes suivantes :

    export template <class T>
    void f(T p)
    {
    // Corps de la fonction.
    return ;
    }

    Note : Aucun compilateur ne gère le mot clé export à ce jour.


    votre commentaire
  • 12.6. Mot-clé typename

    Nous avons déjà vu que le mot clé typename pouvait être utilisé pour introduire les types génériques dans les déclarations template. Cependant, il peut être utilisé dans un autre contexte pour introduire les identificateurs de types inconnus dans les template. En effet, un type générique peut très bien être une classe définie par l'utilisateur, à l'intérieur de laquelle des types sont définis. Afin de pouvoir utiliser ces types dans les définitions des template, il est nécessaire d'utiliser le mot clé typename pour les introduire, car a priori le compilateur ne sait pas que le type générique contient la définition d'un autre type. Ce mot clé doit être placé avant le nom complet du type :

    typename identificateur

    Le mot clé typename est donc utilisé pour signaler au compilateur que l'identificateur identificateur est un type.

    Exemple 12-15. Mot-clé typename

    class A
    {
    public:
    typedef int Y; // Y est un type défini dans la classe A.
    };

    template <class T>
    class X
    {
    typename T::Y i; // La classe template X suppose que le
    // type générique T définisse un type Y.
    };

    X<A> x; // A peut servir à instancier une classe
    // à partir de la classe template X.

    votre commentaire
  • 12.5. Spécialisation des template

    Jusqu'à présent, nous avons défini les classes et les fonctions template d'une manière unique, pour tous les types et toutes les valeurs des paramètres template. Cependant, il peut être intéressant de définir une version particulière d'une classe ou d'une fonction pour un jeu particulier de paramètres template.

    Par exemple, la pile de l'exemple 12.5peut être implémentée beaucoup plus efficacement si elle stocke des pointeurs plutôt que des objets, sauf si les objets sont petits (ou appartiennent à un des types prédéfinis du langage). Il peut être intéressant de manipuler les pointeurs de manière transparente au niveau de la pile, pour que la méthode pop renvoie toujours un objet, que la pile stocke des pointeurs ou des objets. Afin de réaliser cela, il faut donner une deuxième version de la pile pour les pointeurs.

    Le C++ permet tout cela : lorsqu'une fonction ou une classe template a été définie, il est possible de la spécialiser pour un certain jeu de paramètres template. Il existe deux types de spécialisation : les spécialisations totales, qui sont les spécialisations pour lesquelles il n'y a plus aucun paramètre template (ils ont tous une valeur bien déterminée), et les spécialisations partielles, pour lesquelles seuls quelques paramètres template ont une valeur fixée.

    12.5.1. Spécialisation totale

    Les spécialisations totales nécessitent de fournir les valeurs des paramètres template, séparées par des virgules et entre les signes d'infériorité et de supériorité, après le nom de la fonction ou de la classe template. Il faut faire précéder la définition de cette fonction ou de cette classe par la ligne suivante :

    template <>
    qui permet de signaler que la liste des paramètres template pour cette spécialisation est vide (et donc que la spécialisation est totale).

    Par exemple, si la fonction Min définie dans l'exemple 12.4doit être utilisée sur une structure Structure et se baser sur un des champs de cette structure pour effectuer les comparaisons, elle pourra être spécialisée de la manière suivante :

    Exemple 12-12. Spécialisation totale

    struct Structure
    {
    int Clef; // Clef permettant de retrouver des données.
    void *pData; // Pointeur sur les données.
    };

    template <>
    Structure Min<Structure>(Structure s1, Structure s2)
    {
    if (s1.Clef>s2.Clef)
    return s1;
    else
    return s2;
    }

    Note : Pour quelques compilateurs, la ligne déclarant la liste vide des paramètres template ne doit pas être écrite. On doit donc faire des spécialisations totale sans le mot clé template. Ce comportement n'est pas celui spécifié par la norme, et le code écrit pour ces compilateurs n'est donc pas portable.

    12.5.2. Spécialisation partielle

    Les spécialisations partielles permettent de définir l'implémentation d'une fonction ou d'une classe template pour certaines valeurs de leurs paramètres template et de garder d'autres paramètres indéfinis. Il est même possible de changer la nature d'un paramètre template (c'est-à-dire préciser s'il s'agit d'un pointeur ou non) et de forcer le compilateur à prendre une implémentation plutôt qu'une autre selon que la valeur utilisée pour ce paramètre est elle-même un pointeur ou non.

    Comme pour les spécialisations totales, il est nécessaire de déclarer la liste des paramètres template utilisés par la spécialisation. Cependant, à la différence des spécialisations totales, cette liste ne peut plus être vide.

    Comme pour les spécialisations totales, la définition de la classe ou de la fonction template doit utiliser les signes d'infériorité et de supériorité pour donner la liste des valeurs des paramètres template pour la spécialisation.

    Exemple 12-13. Spécialisation partielle

    // Définition d'une classe template :
    template <class T1, class T2, int I>
    class A
    {
    };

    // Spécialisation n°1 de la classe :
    template <class T, int I>
    class A<T, T*, I>
    {
    };

    // Spécialisation n°2 de la classe :
    template <class T1, class T2, int I>
    class A<T1*, T2, I>
    {
    };

    // Spécialisation n°3 de la classe :
    template <class T>
    class A<int, T*, 5>
    {
    };

    // Spécialisation n°4 de la classe :
    template <class T1, class T2, int I>
    class A<T1, T2*, I>
    {
    };

    On notera que le nombre des paramètres template déclarés à la suite du mot clé template peut varier, mais que le nombre de valeurs fournies pour la spécialisation est toujours constant (dans l'exemple précédent, il y en a trois).

    Les valeurs utilisées dans les identificateurs template des spécialisations doivent respecter les règles suivantes :

    • une valeur ne peut pas être exprimée en fonction d'un paramètre template de la spécialisation ;

      template <int I, int J>
      struct B
      {
      };

      template <int I>
      struct B<I, I*2> // Erreur !
      { // Spécialisation incorrecte !
      };
    • le type d'une des valeurs de la spécialisation ne peut pas dépendre d'un autre paramètre ;

      template <class T, T t>
      struct C
      {
      };

      template <class T>
      struct C<T, 1>; // Erreur !
      // Spécialisation incorrecte !
    • la liste des arguments de la spécialisation ne doit pas être identique à la liste implicite de la déclaration template correspondante.

    Enfin, la liste des paramètres template de la déclaration d'une spécialisation ne doit pas contenir des valeurs par défaut. On ne pourrait d'ailleurs les utiliser en aucune manière.

    12.5.3. Spécialisation d'une méthode d'une classe template

    La spécialisation partielle d'une classe peut parfois être assez lourde à employer, en particulier si la structure de données qu'elle contient ne change pas entre les versions spécialisées. Dans ce cas, il peut être plus simple de ne spécialiser que certaines méthodes de la classe et non la classe complète. Cela permet de conserver la définition des méthodes qui n'ont pas lieu d'être modifiées pour les différents types, et d'éviter d'avoir à redéfinir les données membres de la classe à l'identique.

    La syntaxe permettant de spécialiser une méthode d'une classe template est très simple. Il suffit en effet de considérer la méthode comme une fonction template normale, et de la spécialiser en précisant les paramètres template à utiliser pour cette spécialisation.

    Exemple 12-14. Spécialisation de fonction membre de classe template

    #include <iostream>

    using namespace std;

    template <class T>
    class Item
    {
    T item;
    public:
    Item(T);
    void set(T);
    T get(void) const;
    void print(void) const;
    };

    template <class T>
    Item<T>::Item(T i) // Constructeur
    {
    item = i;
    }

    // Accesseurs :

    template <class T>
    void Item<T>::set(T i)
    {
    item = i;
    }

    template <class T>
    T Item<T>::get(void) const
    {
    return item;
    }

    // Fonction d'affichage générique :

    template <class T>
    void Item<T>::print(void) const
    {
    cout << item << endl;
    }

    // Fonction d'affichage spécialisée explicitement pour le type int *
    // et la méthode print :
    template <>
    void Item<int *>::print(void) const
    {
    cout << *item << endl;
    }

    votre commentaire
  • 12.4. Instanciation des template

    La définition des fonctions et des classes template ne génère aucun code tant que tous les paramètres template n'ont pas pris chacun une valeur spécifique. Il faut donc, lors de l'utilisation d'une fonction ou d'une classe template, fournir les valeurs pour tous les paramètres qui n'ont pas de valeur par défaut. Lorsque suffisamment de valeurs sont données, le code est généré pour ce jeu de valeurs. On appelle cette opération l'instanciation des template.

    Plusieurs possibilités sont offertes pour parvenir à ce résultat : l'instanciation implicite et l'instanciation explicite.

    12.4.1. Instanciation implicite

    L'instanciation implicite est utilisée par le compilateur lorsqu'il rencontre une expression qui utilise pour la première fois une fonction ou une classe template, et qu'il doit l'instancier pour continuer son travail. Le compilateur se base alors sur le contexte courant pour déterminer les types des paramètres template à utiliser. Si aucune ambiguïté n'a lieu, il génère le code pour ce jeu de paramètres.

    La détermination des types des paramètres template peut se faire simplement, ou être déduite de l'expression à compiler. Par exemple, les fonctions membres template sont instanciées en fonction du type de leurs paramètres. Si l'on reprend l'exemple de la fonction template Min définie dans l'exemple 12.4, c'est son utilisation directe qui provoque une instanciation implicite.

    Exemple 12-10. Instanciation implicite de fonction template

    int i=Min(2,3);

    Dans cet exemple, la fonction Min est appelée avec les paramètres 2 et 3. Comme ces entiers sont tous les deux de type int, la fonction template Min est instanciée pour le type int. Partout dans la définition de Min, le type générique T est donc remplacé par le type int.

    Si l'on appelle une fonction template avec un jeu de paramètres qui provoque une ambiguïté, le compilateur signale une erreur. Cette erreur peut être levée en surchargeant la fonction template par une fonction qui accepte les mêmes paramètres. Par example, la fonction template Min ne peut pas être instanciée dans le code suivant :

    int i=Min(2,3.0);
    parce que le compilateur ne peut pas déterminer si le type générique T doit prendre la valeur int ou double. Il y a donc une erreur, sauf si une fonction Min(int, double) est définie quelque part. Pour résoudre ce type de problème, on devra spécifier manuellement les paramètres template de la fonction, lors de l'appel. Ainsi, la ligne précédente compile si on la réécrit comme suit :
    int i=Min<int>(2,3.0);
    dans cet exemple, le paramètre template est forcé à int, et 3.0 est converti en entier.

    On prendra garde au fait que le compilateur utilise une politique minimaliste pour l'instanciation implicite des template. Cela signifie qu'il ne créera que le code nécessaire pour compiler l'expression qui exige une instanciation implicite. Par exemple, la définition d'un objet d'une classe template dont tous les types définis provoque l'instanciation de cette classe, mais la définition d'un pointeur sur cette classe ne le fait pas. L'instanciation aura lieu lorsqu'un déréférencement sera fait par l'intermédiaire de ce pointeur. De même, seules les fonctionnalités utilisées de la classe template seront effectivement définies dans le programme final.

    Par exemple, dans le programme suivant :

    #include <iostream>

    using namespace std;

    template <class T>
    class A
    {
    public:
    void f(void);
    void g(void);
    };

    // Définition de la méthode A<T>::f() :
    template <class T>
    void A<T>::f(void)
    {
    cout << "A<T>::f() appelée" << endl;
    }

    // On ne définit pas la méthode A<T>::g()...

    int main(void)
    {
    A<char> a; // Instanciation de A<char>.
    a.f(); // Instanciation de A<char>::f().
    return 0;
    }
    seule la méthode f de la classe template A est instanciée, car c'est la seule méthode utilisée à cet endroit. Ce programme pourra donc parfaitement être compilé, même si la méthode g n'a pas été définie.

    12.4.2. Instanciation explicite

    L'instanciation explicite des template est une technique permettant au programmeur de forcer l'instanciation des template dans son programme. Pour réaliser une instanciation explicite, il faut spécifier explicitement tous les paramètres template à utiliser. Cela se fait simplement en donnant la déclaration du template, précédée par le mot clé template :

    template nom<valeur[, valeur[...]]>;

    Par exemple, pour forcer l'instanciation d'une pile telle que celle définie dans l'exemple 15, il faudra préciser le type des éléments entre crochets après le nom de la classe :

    template Stack<int>;  // Instancie la classe Stack<int>.

    Cette syntaxe peut être simplifiée pour les fonctions template, à condition que tous les paramètres template puissent être déduits par le compilateur des types des paramètres utilisés dans la déclaration de la fonction. Ainsi, il est possible de forcer l'instanciation de la fonction template Min de la manière suivante :

    template int Min(int, int);

    Dans cet exemple, la fonction template Min est instanciée pour le type int, puisque ses paramètres sont de ce type.

    Lorsqu'une fonction ou une classe template a des valeurs par défaut pour ses paramètres template, il n'est pas nécessaire de donner une valeur pour ces paramètres. Si toutes les valeurs par défaut sont utilisées, la liste des valeurs peut être vide (mais les signes d'infériorité et de supériorité doivent malgré tout être présents).

    Exemple 12-11. Instanciation explicite de classe template

    template<class T = char>
    class Chaine;

    template Chaine<>; // Instanciation explicite de Chaine<char>.

    12.4.3. Problèmes soulevés par l'instanciation des template

    Les template doivent impérativement être définis lors de leur instanciation pour que le compilateur puisse générer le code de l'instance. Cela signifie que les fichiers d'en-tête doivent contenir non seulement la déclaration, mais également la définition complète des template. Cela a plusieurs inconvénients. Le premier est bien entendu que l'on ne peut pas considérer les template comme les fonctions et les classes normales du langage, pour lesquels il est possible de séparer la déclaration de la définition dans des fichiers séparés. Le deuxième inconvénient est que les instances des template sont compilées plusieurs fois, ce qui diminue d'autant plus les performances des compilateurs. Enfin, ce qui est le plus grave, c'est que les instances des template sont en multiples exemplaires dans les fichiers objets générés par le compilateur, et accroissent donc la taille des fichiers exécutables à l'issue de l'édition de liens. Cela n'est pas gênant pour les petits programmes, mais peut devenir rédhibitoire pour les programmes assez gros.

    Le premier problème n'est pas trop gênant, car il réduit le nombre de fichiers sources, ce qui n'est en général pas une mauvaise chose. Notez également que les template ne peuvent pas être considérés comme des fichiers sources classiques, puisque sans instanciation, ils ne génèrent aucun code machine (ce sont des classes de classes, ou « métaclasses »). Mais ce problème peut devenir ennuyant dans le cas de bibliothèques template écrites et vendues par des sociétés désireuses de conserver leur savoir-faire. Pour résoudre ce problème, le langage donne la possibilité d'exporter les définitions des template dans des fichiers complémentaires. Nous verrons la manière de procéder dans la Section 12.7.

    Le deuxième problème peut être résolu avec l'exportation des template, ou par tout autre technique d'optimisation des compilateurs. Actuellement, la plupart des compilateurs sont capables de générer des fichiers d'en-tête précompilés, qui contiennent le résultat de l'analyse des fichiers d'en-tête déjà lus. Cette technique permet de diminuer considérablement les temps de compilation, mais nécessite souvent d'utiliser toujours le même fichier d'en-tête au début des fichiers sources.

    Le troisième problème est en général résolu par des techniques variées, qui nécessitent des traitements complexes dans l'éditeur de liens ou le compilateur. La technique la plus simple, utilisée par la plupart des compilateurs actuels, passe par une modification de l'éditeur de liens pour qu'il regroupe les différentes instances des mêmes template. D'autres compilateurs, plus rares, gèrent une base de données dans laquelle les instances de template générées lors de la compilation sont stockées. Lors de l'édition de liens, les instances de cette base sont ajoutées à la ligne de commande de l'éditeur de liens afin de résoudre les symboles non définis. Enfin, certains compilateurs permettent de désactiver les instanciations implicites des template. Cela permet de laisser au programmeur la responsabilité de les instancier manuellement, à l'aide d'instanciations explicites. Ainsi, les template peuvent n'être définies que dans un seul fichier source, réservé à cet effet. Cette dernière solution est de loin la plus sûre, et il est donc recommandé d'écrire un tel fichier pour chaque programme.

    Ce paragraphe vous a présenté trois des principaux problèmes soulevés par l'utilisation des template, ainsi que les solutions les plus courantes qui y ont été apportées. Il est vivement recommandé de consulter la documentation fournie avec l'environnement de développement utilisé, afin à la fois de réduire les temps de compilation et d'optimiser les exécutables générés.


    votre commentaire