• Annexe B. Draft Papers

    Les Draft Papers sont vraiment une source d'informations très précise, mais ils ne sont pas vraiment structurés. En fait, ils ne sont destinés qu'aux éditeurs de logiciels désirant réaliser un compilateur, et la structure du document ressemble à un texte de loi (fortement technique en prime). Les exemples y sont rares, et quand il y en a, on ne sait pas à quel paragraphe ils se réfèrent. Enfin, nombre de termes non définis sont utilisés, et il faut lire le document pendant quelques 40 pages avant de commencer à le comprendre.

    Afin de faciliter leur lecture, je donne ici quelques définitions, ainsi que la structure des Draft Papers.

    Les Draft Papers sont constitués de deux grandes parties. La première traite du langage, de sa syntaxe et de sa sémantique. La deuxième partie décrit la bibliothèque standard C++.

    La syntaxe est décrite dans la première partie de la manière BNF. Il vaut mieux être familiarisé avec cette forme de description pour la comprendre. Cela ne causera pas de problème cependant si l'on maîtrise déjà la syntaxe du C++.

    Lors de la lecture de la deuxième partie, on ne s'attardera pas trop sur les fonctionnalités de gestion des langues et des jeux de caractères (locales). Elles ne sont pas nécessaires à la compréhension de la bibliothèque standard. Une fois les grands principes de la bibliothèque assimilés, les notions de locale pourront être approfondies.

    Les termes suivants sont souvent utilisés et non définis (ou définis au milieu du document d'une manière peu claire). Leurs définitions pourront être d'un grand secours lors de lecture de la première partie des Draft Papers :

    • cv, cv qualified : l'abréviation cv signifie ici const ou volatile. Ce sont donc les propriétés de constance et de volatilité ;

    • un agrégat est un tableau ou une classe qui n'a pas de constructeurs, pas de fonctions virtuelles, et pas de donnée non statique private ou protected ;

    • POD : cette abréviation signifie plain ol' data, ce qui n'est pas compréhensible a priori. En fait, un type POD est un type relativement simple, pour lequel aucun traitement particulier n'est nécessaire (pas de constructeur, pas de virtualité, etc.). La définition des types POD est récursive : une structure ou une union est un type POD si c'est un agrégat qui ne contient pas de pointeur sur un membre non statique, pas de référence, pas de type non POD, pas de constructeur de copie et pas de destructeur.

    Les autres termes sont définis lorsqu'ils apparaissent pour la première fois dans le document.


    votre commentaire
  • Annexe A. Priorités des opérateurs

    Cette annexe donne la priorité des opérateurs du langage C++, dans l'ordre décroissant. Cette priorité intervient dans l'analyse de toute expression et dans la détermination de son sens. Cependant, l'analyse des expressions peut être modifiée en changeant les priorités à l'aide de parenthèses.

    Tableau A-1. Opérateurs du langage

    OpérateurNom ou signification
    ::Opérateur de résolution de portée
    []Opérateur d'accès aux éléments de tableau
    ()Opérateur d'appel de fonction
    type()Opérateur de transtypage explicite
    .Opérateur de sélection de membre
    ->Opérateur de sélection de membre par déréférencement
    ++Opérateur d'incrémentation post-fixe
    --Opérateur de décrémentation post-fixe
    newOpérateur de création dynamique d'objets
    new[]Opérateur de création dynamique de tableaux
    deleteOpérateur de destruction des objets créés dynamiquement
    delete[]Opérateur de destruction des tableaux créés dynamiquement
    ++Opérateur d'incrémentation préfixe
    --Opérateur de décrémentation préfixe
    *Opérateur de déréférencement
    &Opérateur d'adresse
    +Opérateur plus unaire
    -Opérateur négation unaire
    !Opérateur de négation logique
    ~Opérateur de complément à un
    sizeofOpérateur de taille d'objet
    sizeofOpérateur de taille de type
    typeidOpérateur d'identification de type
    (type)Opérateur de transtypage
    const_castOpérateur de transtypage de constance
    dynamic_castOpérateur de transtypage dynamique
    reinterpret_castOpérateur de réinterprétation
    static_castOpérateur de transtypage statique
    .*Opérateur de sélection de membre par pointeur sur membre
    ->*Opérateur de sélection de membre par pointeur sur membre par déréférencement
    *Opérateur de multiplication
    /Opérateur de division
    %Opérateur de reste de la division entière
    +Opérateur d'addition
    -Opérateur de soustraction
    <<Opérateur de décalage à gauche
    >>Opérateur de décalage à droite
    <Opérateur d'infériorité
    >Opérateur de supériorité
    <=Opérateur d'infériorité ou d'égalité
    >=Opérateur de supériorité ou d'égalité
    ==Opérateur d'égalité
    !=Opérateur d'inégalité
    &Opérateur et binaire
    ^Opérateur ou exclusif binaire
    |Opérateur ou inclusif binaire
    &&Opérateur et logique
    ||Opérateur ou logique
    ?:Opérateur ternaire
    =Opérateur d'affectation
    *=Opérateur de multiplication et d'affectation
    /=Opérateur de division et d'affectation
    %=Opérateur de modulo et d'affectation
    +=Opérateur d'addition et d'affectation
    -=Opérateur de soustraction et d'affectation
    <<=Opérateur de décalage à gauche et d'affectation
    >>=Opérateur de décalage à droite et d'affectation
    &=Opérateur de et binaire et d'affectation
    |=Opérateur de ou inclusif binaire et d'affectation
    ^=Opérateur de ou exclusif binaire et d'affectation
    ,Opérateur virgule

    votre commentaire
  • Chapitre 19. Conclusion

    Pour terminer, je rappellerai les principales règles pour réaliser de bons programmes. Sans organisation, aucun langage, aussi puissant soit-il, ne peut garantir le succès d'un projet. Voici donc quelques conseils :

    • commentez votre code, mais ne tuez pas le commentaire en en mettant là où les opérations sont vraiment très simples ou décrites dans un document externe. Marquez les références aux documents externes dans les commentaires ;

    • analysez le problème avant de commencer la programmation. Cela comprend plusieurs étapes. La première est de réfléchir aux structures de données à utiliser et aux opérations qu'on va leur appliquer (il faut donc identifier les classes). Il faut ensuite établir les relations entre les classes ainsi identifiées et leurs communications. Pour cela, on pourra faire des diagrammes d'événements qui identifient les différentes étapes du processus permettant de traiter une donnée. Enfin, on décrira chacune des méthodes des classes fonctionnellement, afin de savoir exactement quelles sont leurs entrées et les domaines de validité de celles-ci, leurs sorties, leurs effets de bords et les opérations effectuées. Enfin seulement on passera au codage. Si le codage implique de corriger les résultats des étapes précédentes, c'est que la conception a été incorrecte ou incomplète : il vaut mieux retourner en phase de conception un peu pour voir l'impact des modifications à faire. Cela permet de ne pas passer à coté d'un effet de bord inattendu, et donc d'éviter de perdre du temps dans la phase de mise au point ;

    • ne considérez aucun projet, même un petit projet ou un projet personnel, comme un projet qui échappe à ces règles. Si vous devez interrompre le développement d'un projet pour une raison quelconque, vous serez content de retrouver le maximum d'informations sur lui. Il en est de même si vous désirez améliorer un ancien projet. Et si la conception a été bien faite, cette amélioration ne sera pas une verrue sur l'ancienne version du logiciel, contrairement à ce qui se passe trop souvent.

    Voilà. Vous connaissez à présent la plupart des fonctionnalités du C++. J'espère que la lecture de ce cours vous aura été utile et agréable. Si vous voulez en savoir plus, consultez les Draft Papers, mais sachez qu'ils sont réellement difficiles à lire. Ils ne peuvent vraiment pas être pris pour un support de cours. L'annexe b décrit l'organisation générale de ce document et donne quelques renseignements pour faciliter leur lecture.

    Bonne continuation...


    votre commentaire
  • 18.5. Opérations ensemblistes

    En mathématiques, il est possible d'effectuer différents types d'opérations sur les ensembles. Ces opérations comprennent la détermination de l'inclusion d'un ensemble dans un autre, leur union (c'est-à-dire le regroupement de tous leurs éléments), leur intersection (la sélection de leurs éléments communs), leur différence (la suppression des éléments d'un ensemble qui appartiennent aussi à un autre ensemble) et leur partitionnement (le découpage d'un ensemble en sous-ensemble dont les éléments vérifient une propriété discriminante).

    La bibliothèque standard fournit tout un ensemble d'algorithmes qui permettent d'effectuer les opérations ensemblistes classiques sur les conteneurs triés. Tous ces algorithmes sont décrits ci-dessous et sont classés selon la nature des opérations qu'ils réalisent.

    Note : Remarquez ici que la notion de tri est importante : les algorithmes s'appuient sur cette propriété des conteneurs pour effectuer leur travail. En contrepartie de cette contrainte, les performances de ces algorithmes sont excellentes.

    18.5.1. Opérations d'inclusion

    L'inclusion d'un ensemble dans un autre peut être réalisée à l'aide de l'algorithme includes. Cet algorithme est déclaré comme suit dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2>
    bool includes(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2);

    template <class InputIterator1, class InputIterator2, class Compare>
    bool includes(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2, Compare c);

    L'algorithme includes prend en paramètre deux couples d'itérateurs permettant de définir les séquences d'éléments des deux ensembles sur lesquels il doit travailler. La valeur retournée par cet algorithme est true si tous les éléments de la séquence identifiée par les itérateurs premier2 et dernier2 sont également présents dans la séquence identifiée par les itérateurs premier1 et dernier1. L'algorithme considère qu'un élément est présent dans un ensemble s'il existe au moins un élément de cet ensemble qui lui est identique. Chaque élément utilisé de l'ensemble ne l'est qu'une seule fois, ainsi, si l'ensemble dont on teste l'inclusion dispose de plusieurs copies du même élément, il faut qu'il y en ait autant dans l'ensemble conteneur pour que le test d'inclusion soit valide.

    Bien entendu, il est possible d'utiliser une autre relation que l'égalité pour déterminer l'appartenance d'un élément à un ensemble, pour cela, il suffit de fournir un foncteur binaire en dernier paramètre. Ce prédicat doit prendre deux éléments en paramètre et renvoyer true si le premier élément est inférieur au second, et false dans le cas contraire.

    Note : Il est important que le foncteur d'infériorité spécifié soit compatible avec la relation d'ordre utilisée pour le tri des éléments des conteneurs. Si ce n'est pas le cas, l'algorithme peut ne pas fonctionner correctement.

    Exemple 18-28. Algorithme de détermination d'inclusion

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int t2[3] = {4, 5, 6};
    if (includes(t1, t1+10, t2, t2+3))
    cout << "t1 contient t2" << endl;
    return 0;
    }

    La complexité de l'algorithme includes est n+m, où n et m sont respectivement les tailles des deux conteneurs qui lui sont fournis en paramètre.

    18.5.2. Opérations d'intersection

    L'intersection de deux ensembles peut être réalisée à l'aide de l'algorithme set_intersection. Cet algorithme est déclaré de la manière suivante dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2,
    class OutputIterator>
    OutputIterator set_intersection(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination);

    template <class InputIterator1, class InputIterator2,
    class OutputIterator, class Compare>
    OutputIterator set_intersection(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination, Compare c);

    Cet algorithme prend en paramètre les itérateurs de début et de fin des deux conteneurs dont l'intersection doit être déterminée, ainsi qu'un itérateur référençant l'emplacement destination où les éléments de l'intersection doivent être stockés. Pour ceux qui le désirent, il est également possible de spécifier un foncteur que l'algorithme utilisera pour effectuer les comparaisons d'infériorité entre les éléments des deux conteneurs fournis en paramètre. Ce foncteur devra bien entendu être compatible avec la relation d'ordre selon laquelle les conteneurs passés en paramètre sont triés.

    L'algorithme copie à l'emplacement destination tous les éléments du premier conteneur qui font également partie du deuxième. Le critère d'appartenance à un ensemble est, comme pour l'algorithme includes, le fait qu'il existe au moins un élément dans le deuxième ensemble égal à l'élément considéré. De même, si plusieurs copies d'un même élément se trouvent dans chaque ensemble, le nombre de copies de l'intersection sera le plus petit nombre de copies de l'élément dans les deux ensembles sources.

    Exemple 18-29. Algorithme d'intersection d'ensembles

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[10] = {2, 4, 6, 8, 9, 10, 15, 15, 15, 17};
    int t2[10] = {1, 4, 5, 8, 11, 15, 15, 16, 18, 19};
    int t[10];
    // Effectue l'intersection de t1 et de t2 :
    int *fin = set_intersection(t1, t1+10, t2, t2+10, t);
    // Affiche le résultat :
    int *p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    return 0;
    }

    La complexité de l'algorithme est n+m, où n et m sont respectivement les tailles des deux conteneurs qui lui sont fournis en paramètre.

    18.5.3. Opérations d'union et de fusion

    La bibliothèque standard fournit plusieurs algorithmes permettant de réaliser l'union de deux ensembles. Ces variantes se distinguent par la manière qu'elles ont de traiter le cas des éléments en multiples exemplaires.

    L'algorithme set_union considère que les éléments équivalents des deux ensembles sont les mêmes entités et ne les place qu'une seule fois dans l'ensemble résultat de l'union. Toutefois, si ces éléments sont en plusieurs exemplaires dans un des ensembles source, ils apparaîtront également en plusieurs exemplaires dans le résultat. Autrement dit, le nombre d'éléments présents dans l'ensemble destination est le nombre maximum du compte de ses occurrences dans chacun des deux ensembles source.

    Inversement, l'algorithme merge effectue une union au sens large et ajoute les éléments de chaque ensemble dans l'ensemble résultat sans considérer leurs valeurs. Ainsi, le nombre d'éléments du résultat est strictement égal à la somme des nombres des éléments de chaque conteneur source.

    Afin de distinguer ces deux comportements, on peut dire que l'algorithme set_union réalise l'union des deux ensembles, alors que l'algorithme merge réalise leur fusion.

    Tous ces algorithmes sont déclarés comme suit dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2,
    class OutputIterator>
    OutputIterator set_union(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination);

    template <class InputIterator1, class InputIterator2,
    class OutputIterator, class Compare>
    OutputIterator set_union(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination, Compare c);

    template <class InputIterator1, class InputIterator2, class OutputIterator>
    OutputIterator merge(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination);

    template <class InputIterator1, class InputIterator2,
    class OutputIterator, class Compare>
    OutputIterator merge(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 dernier2, InputIterator2 premier2,
    OutputIterator destination, Compare c);

    Comme vous pouvez le constater, ils prennent tous en paramètre les itérateurs permettant de spécifier les deux ensembles ainsi qu'un itérateur destination indiquant l'emplacement où les éléments de l'union ou de la fusion doivent être stockés. Enfin, si le programmeur le désire, il peut également donner le foncteur définissant la relation d'ordre selon laquelle les ensembles sont triés.

    Exemple 18-30. Algorithmes d'union et de fusion d'ensembles

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[4] = {1, 2, 5, 5};
    int t2[6] = {3, 4, 5, 5, 5, 7};
    int t[10];
    // Effectue l'union de t1 et de t2 :
    int *fin = set_union(t1, t1+4, t2, t2+6, t);
    // Affiche le résultat :
    int *p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    // Effectue la fusion de t1 et de t2 :
    fin = merge(t1, t1+4, t2, t2+6, t);
    // Affiche le résultat :
    p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    return 0;
    }

    La bibliothèque standard fournit également une version modifiée de l'algorithme merge dont le but est de fusionner deux parties d'une même séquence d'éléments triées indépendamment l'une de l'autre. Cet algorithme permet d'effectuer la fusion sur place, et ne travaille donc que sur un seul conteneur. Il s'agit de l'algorithme inplace_merge, qui est déclaré comme suit :

    template <class BidirectionalIterator>
    void inplace_merge(BidirectionalIterator premier,
    BidirectionalIterator separation,
    BidirectionalIterator dernier);

    template <class BidirectionalIterator, class Compare>
    void inplace_merge(BidirectionalIterator premier,
    BidirectionalIterator separation,
    BidirectionalIterator dernier, Compare c);

    Cet algorithme effectue la fusion des deux ensembles identifiés respectivement par les itérateurs premier et separation d'une part, et par les itérateurs separation et dernier d'autre part. Enfin, si besoin est, il est possible de spécifier le foncteur selon lequel ces deux ensembles sont triés.

    Exemple 18-31. Algorithme de réunification de deux sous-ensembles

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t[10] = {1, 5, 9, 0, 2, 3, 4, 6, 7, 8};
    // Fusionne les deux sous-ensembles de t
    // (la séparation est au troisième élément) :
    inplace_merge(t, t+3, t+10);
    // Affiche le résultat :
    int i;
    for (i=0; i<10; ++i)
    {
    cout << t[i] << " ";
    }
    cout << endl;
    return 0;
    }

    Tous les algorithmes d'union et de fusion ont une complexité n+m, où n et m sont les tailles des deux ensembles à fusionner ou à réunir.

    18.5.4. Opérations de différence

    La différence entre deux ensembles peut être réalisée avec l'algorithme set_difference. Cet algorithme supprime du premier ensemble tous les éléments du second, si nécessaire. Chaque élément n'est supprimé qu'une seule fois, ainsi, si le premier ensemble contient plusieurs éléments identiques et que le deuxième ensemble en contient moins, les éléments résiduels après suppression seront présents dans la différence.

    La bibliothèque standard fournit également un algorithme de suppression symétrique, l'algorithme set_symmetric_difference, qui construit un nouvel ensemble contenant tous les éléments des deux ensembles qui ne se trouvent pas dans l'autre. Il s'agit en fait de l'union des deux différences des deux ensembles.

    Note : Remarquez que le mot « symmetric » s'écrit avec deux 'm' en anglais. Ne vous étonnez donc pas d'obtenir des erreurs de compilation si vous écrivez set_symmetric_difference à la française !

    Les algorithmes set_difference et set_symmetric_difference sont déclarés comme suit dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2,
    class OutputIterator>
    OutputIterator set_difference(
    InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination);

    template <class InputIterator1, class InputIterator2,
    class OutputIterator, class Compare>
    OutputIterator set_difference(
    InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination, Compare c);

    template <class InputIterator1, class InputIterator2, class OutputIterator>
    OutputIterator set_symmetric_difference(
    InputIterator1 premier, InputIterator1 dernier,
    InputIterator2 premier, InputIterator2 dernier2,
    OutputIterator destination);

    template <class InputIterator1, class InputIterator2,
    class OutputIterator, class Compare>
    OutputIterator set_symmetric_difference(
    InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    OutputIterator destination, Compare c);

    Ils prennent tous deux paires d'itérateurs identifiant les deux ensembles dont la différence doit être calculée ainsi qu'un itérateur référençant l'emplacement destination dans lequel le résultat doit être placé. Comme à l'accoutumée, il est possible d'indiquer le foncteur permettant à l'algorithme de réaliser les tests d'infériorité entre deux éléments et selon lequel les ensembles sont triés. La complexité de ces algorithmes est n+m, où n et m sont les nombres d'éléments des deux ensembles sur lesquels les algorithmes opèrent.

    Exemple 18-32. Algorithmes de différence d'ensembles

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[10] = {0, 1, 5, 7, 7, 7, 8, 8, 9, 10};
    int t2[10] = {0, 2, 3, 7, 9, 11, 12, 12, 13, 14};
    int t[20];
    // Calcule la différence de t1 et de t2 :
    int *fin = set_difference(t1, t1+10, t2, t2+10, t);
    // Affiche le résultat :
    int *p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    // Calcule la différence symétrique de t1 et t2 :
    fin = set_symmetric_difference(t1, t1+10, t2, t2+10, t);
    // Affiche le résultat :
    int *p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    // Calcule la différence symétrique de t1 et t2 :
    fin = set_symmetric_difference(t1, t1+10, t2, t2+10, t);
    // Affiche le résultat :
    p = t;
    while (p != fin)
    {
    cout << *p << " ";
    ++p;
    }
    cout << endl;
    return 0;
    }

    18.5.5. Opérations de partitionnement

    L'algorithme partition de la bibliothèque standard permet de séparer les éléments d'un ensemble en deux sous-ensembles selon un critère donné. Les éléments vérifiant ce critère sont placés en tête de l'ensemble, et les éléments qui ne le vérifient pas sont placés à la fin. Cet algorithme est déclaré comme suit dans l'en-tête algorithm :

    template <class ForwardIterator, class Predicate>
    ForwardIterator partition(ForwardIterator premier,
    ForwardIterator dernier, Predicate p);

    Les paramètres qui doivent être fournis à cet algorithme sont les itérateurs référençant le premier et le dernier élément de l'ensemble à partitionner, ainsi qu'un foncteur unaire permettant de déterminer si un élément vérifie le critère de partitionnement ou non. La valeur retournée est la position de la séparation entre les deux sous-ensembles générés par l'opération de partition.

    Exemple 18-33. Algorithme de partitionnement

    #include <iostream>
    #include <functional>
    #include <algorithm>

    using namespace std;

    bool parity_even(int i)
    {
    return (i & 1) == 0;
    }

    int main(void)
    {
    int t[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // Partitionne le tableau en nombre pairs
    // et nombre impairs :
    partition(t, t+10, ptr_fun(&parity_even));
    // Affiche le résultat :
    int i;
    for (i=0; i<10; ++i)
    cout << t[i] << " ";
    cout << endl;
    return 0;
    }

    La complexité de l'algorithme partition est linéaire en fonction du nombre d'éléments de l'ensemble à partitionner. Cependant, l'opération de partitionnement n'est pas stable, c'est-à-dire que l'ordre relatif des éléments de même valeur et sur lesquels le prédicat du critère de partitionnement donne le même résultat n'est pas conservé. La bibliothèque standard fournit donc un autre algorithme, stable celui-là, mais qui s'exécute avec une complexité légèrement supérieure. Il s'agit de l'algorithme stable_partition, qui est déclaré comme suit dans l'en-tête algorithm :

    template <class ForwardIterator, class Predicate>
    ForwardIterator stable_partition(ForwardIterator premier,
    ForwardIterator dernier, Predicate p);

    Comme vous pouvez le constater, cet algorithme s'utilise exactement de la même manière que l'algorithme partition. Toutefois, il garantit l'ordre relatif des éléments au sein des sous-ensembles générés par l'opération de partitionnement. La complexité de cet algorithme est n s'il dispose de suffisamment de mémoire, et n×ln(n) dans le cas contraire (n étant la taille de l'ensemble à partitionner).


    votre commentaire
  • 8.4. Opérations de comparaison

    Afin de faciliter la comparaison de conteneurs de natures différentes pour lesquels, de surcroît, il n'existe pas forcément d'opérateurs de comparaison, la bibliothèque standard fournit plusieurs algorithmes de comparaison. Ces algorithmes sont capables d'effectuer une comparaison élément à élément des différents conteneurs pour vérifier leur égalité en terme d'éléments contenus, ou de déterminer une relation d'ordre au sens lexicographique. Enfin, il est possible de déterminer les éléments par lesquels deux conteneurs se différencient.

    L'algorithme général de comparaison des conteneurs est l'algorithme equal. Cet algorithme est déclaré comme suit dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2>
    bool equal(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2);

    template <class InputIterator1, class InputIterator2, class BinaryPredicate>
    bool equal(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, BinaryPredicate p);

    Comme vous pouvez le constater d'après cette déclaration, l'algorithme equal prend en paramètre un couple d'itérateurs décrivant la séquence d'éléments qui doivent être pris en compte dans la comparaison ainsi qu'un itérateur sur le premier élément du deuxième conteneur. Les éléments référencés successivement par les itérateurs premier1 et premier2 sont ainsi comparés, jusqu'à ce qu'une différence soit détectée ou que l'itérateur dernier1 du premier conteneur soit atteint. La valeur retournée est true si les deux séquences d'éléments des deux conteneurs sont égales élément à élément, et false sinon. Bien entendu, il est possible de spécifier un foncteur binaire que l'algorithme devra utiliser pour réaliser les comparaisons entre les éléments des deux conteneurs. S'il est spécifié, ce foncteur est utilisé pour déterminer si les éléments comparés sont égaux ou non.

    Note : Notez bien ici que le foncteur fourni permet de tester l'égalité de deux éléments et non l'infériorité, comme c'est le cas avec la plupart des autres algorithmes.

    S'il s'avère que les deux conteneurs ne sont pas égaux membre à membre, il peut être utile de déterminer les itérateurs des deux éléments qui ont fait échouer le test d'égalité. Cela peut être réalisé à l'aide de l'algorithme mismatch dont on trouvera la déclaration dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2>
    pair<InputIterator1, InputIterator2>
    mismatch(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2);

    template <class InputIterator1, class InputIterator2, class BinaryPredicate>
    pair<InputIterator1, InputIterator2>
    mismatch(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, BinaryPredicate p);

    Cet algorithme fonctionne exactement de la même manière que l'algorithme equal. Cependant, contrairement à ce dernier, sa valeur de retour est une paire d'itérateurs des deux conteneurs référençant les éléments respectifs qui ne sont pas égaux au sens de l'opération de comparaison utilisée par l'algorithme (que ce soit l'opérateur d'égalité ou le foncteur fourni en paramètre). Si les deux conteneurs sont effectivement égaux, la valeur retournée est la paire contenant l'itérateur dernier1 et l'itérateur correspondant dans le deuxième conteneur.

    Exemple 18-26. Algorithme de comparaison de conteneurs

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[10] = {5, 6, 4, 7, 8, 9, 2, 1, 3, 0};
    int t2[10] = {5, 6, 4, 7, 9, 2, 1, 8, 3, 0};
    // Compare les deux tableaux :
    if (!equal(t1, t1+10, t2))
    {
    // Détermine les éléments différents :
    pair<int *, int *> p =
    mismatch(t1, t1+10, t2);
    cout << *p.first << " est différent de " <<
    *p.second << endl;
    }
    return 0;
    }

    Enfin, la bibliothèque standard fournit un algorithme de comparaison général permettant de déterminer si un conteneur est inférieur à un autre conteneur selon la relation d'ordre lexicographique induite par l'opérateur d'infériorité du type de leurs éléments. Rappelons que l'ordre lexicographique est celui utilisé par le dictionnaire : les éléments sont examinés un à un et dans leur ordre d'apparition et la comparaison s'arrête dès que deux éléments différents sont trouvés. En cas d'égalité totale, le plus petit des conteneurs est celui qui contient le moins d'éléments.

    L'algorithme de comparaison lexicographique est l'algorithme lexicographical_compare. Il est déclaré comme suit dans l'en-tête algorithm :

    template <class InputIterator1, class InputIterator2>
    bool lexicographical_compare(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2);

    template <class InputIterator1, class InputIterator2, class Compare>
    bool lexicographical_compare(InputIterator1 premier1, InputIterator1 dernier1,
    InputIterator2 premier2, InputIterator2 dernier2,
    Compare c);

    Cet algorithme prend en paramètre deux couples d'itérateurs grâce auxquels le programmeur peut spécifier les deux séquences d'éléments à comparer selon l'ordre lexicographique. Comme à l'accoutumée, il est également possible de fournir un foncteur à utiliser pour les tests d'infériorité entre les éléments des deux conteneurs. La valeur retournée par l'algorithme lexicographical_compare est true si le premier conteneur est strictement plus petit que le deuxième et false sinon.

    Exemple 18-27. Algorithme de comparaison lexicographique

    #include <iostream>
    #include <algorithm>

    using namespace std;

    int main(void)
    {
    int t1[10] = {5, 6, 4, 7, 8, 9, 2, 1, 3, 0};
    int t2[10] = {5, 6, 4, 7, 9, 2, 1, 8, 3, 0};
    // Compare les deux tableaux :
    if (lexicographical_compare(t1, t1+10, t2, t2+10))
    {
    cout << "t1 est plus petit que t2" << endl;
    }
    return 0;
    }

    Tous ces algorithmes de comparaison s'exécutent avec une complexité linéaire en fonction du nombre d'éléments à comparer.


    votre commentaire