Outils d'utilisateurs

Outils du Site


comparer

Ceci est une ancienne révision du document !


Chapitre précédent Sommaire principal Chapitre suivant

Comparer des collections

Pour comparer si deux “choses” sont égales, le plus simple sera généralement d'utiliser l'opérateur de comparaison == que vous avez déjà vu. Cet opérateur fonctionne aussi bien pour les types fondamentaux du C++ (build-in types, comme int, float, etc.) que pour certains types de classe (les classes à sémantique de valeur). Il est également possible d'utiliser des algorithmes dans les cas d'utilisation plus spécifiques.

L'opérateurs d'égalité

Dans les chapitres précédents, vous avez vu l'utilisation de l'opérateur de comparaison == (egal-to operator). Les collections de la bibliothèque standard, comme vector, array ou string, fournissent également cet opérateur. Vous pouvez donc écrire :

main.cpp
#include <iostream>
#include <string>
 
int main() {
    std::string const s1 { "salut" };
    std::string const s2 { "salut" };
    std::string const s3 { "hello" };
    std::cout << std::boolalpha;
    std::cout << (s1 == s2) << std::endl;
    std::cout << (s1 == s3) << std::endl;
}

affiche :

true
false

Remarque : ne pas oublier les parenthèses autour du test d'égalité pour string. L'opérateur « ayant un sens pour cette classe, le compilateur ne pourra pas savoir si vous souhaitez écrire :

std::cout << (s1 == s2) << std::endl;
std::cout << s1 == (s2 << std::endl);

De même pour les collections :

main.cpp
#include <iostream>
#include <vector>
 
int main() {
    std::vector<int> const v1 { 1, 2, 3, 4 };
    std::vector<int> const v2 { 1, 2, 3, 4 };
    std::vector<int> const v3 { 4, 3, 2, 1 };
    std::cout << std::boolalpha;
    std::cout << (v1 == v2) << std::endl;
    std::cout << (v1 == v3) << std::endl;
}

affiche :

true
false

Avec les collections, l'opérateur d'égalité compare les éléments de la collection un par un et s'il trouve une différence, il retourne false. S'il arrive à la fin de la collection sans trouver de différence, il retourne true.

Pour vérifier si deux chaînes sont différentes, il est possible d'utiliser l'opérateur booléen de négation ! : !(s1 == s2). Plus simplement, on peut directement utiliser l'opérateur de comparaison != (not-equal-to operator).

main.cpp
#include <iostream>
#include <string>
 
int main() {
    std::string const s1 { "salut" };
    std::string const s2 { "salut" };
    std::string const s3 { "hello" };
    std::cout << std::boolalpha << (s1 != s2) << std::endl;
    std::cout << (s1 != s3) << std::endl;
}

affiche :

false
true

On voit ici que les opérateurs == et != sont étroitement liés. Lorsque l'un de ces deux opérateurs est utilisable, on peut s'attendre à ce que l'autre opérateur le soit également. On dit qu'une classe qui propose l'opérateur d'égalité == qu'elle respecte le concept de “comparable par égalité” (EqualityComparable). Ce concept précise que l'opérateur d'égalité doit suivre les propriétés suivantes :

  • réflexivité : quelque soit a, a == a est toujours vrai ;
  • commutativité : si a == b, alors b == a ;
  • transitivité : si a == b et b == c, alors a == c.

Ce concept est assez classique, on le retrouve en mathématique dans la théorie des ensembles. On voit ici un point important : lorsqu'une classe définit un opérateur ==, on s'attend à ce qu'elle suive un certain nombre de règles. On dit qu'elle suis une sémantique, cela facilite son utilisation. Du point de vue de l'utilisateur de cette classe, on pourra utiliser n'importe quelle classe respectant cette sémantique de la même façon. Du point de vue du concepteur de la classe (ce que vous apprendrez à faire dans la suite de ce cours), il suffit de définir les sémantiques que l'on souhaite donner à notre classe, l'écriture de la classe en découlera.

Au contraire, le non respect d'une sémantique sera très perturbant pour l'utilisateur - et une source d'erreur sans fin. Imaginez que l'opérateur == ne réalise pas un test d'égalité, mais permet de faire la concaténation de deux chaînes ?

Bien sûr, ces considérations s'appliquent à l'ensemble des sémantiques usuelles, en particulier celle que l'on connait en mathématique (addition avec +, soustraction avec -, etc.)

La sémantique de valeur

Un concept complexe peut être décomposé en une série de concepts plus simple (par exemple “est comparable par égalité” est composé des concepts “est réflexif”, “est commutatif” et “est transitif” que l'on a vu avant). De la même façon, il est possible de combiner des concepts pour créer de nouveaux concepts plus complexe. Un concept peut également autorisé ou interdire l'utilisation d'autres concepts (par exemple, le concept “est égal” autorise l'utilisation du concept “est différent”).

C'est la cas du concept “est comparable par égalité”, qui fait parti d'un concept plus général : la sémantique de valeur. Cette sémantique s'applique à tout ce qui représente une valeur : un entier, un nombre réel, un nombre complexe, une chaîne de caractères, un tableau de données, etc. La sémantique de valeur autorise les concepts suivants :

Constructible par défaut (DefaultConstructible). On peut initialiser avec une valeur par défaut (on parle aussi de “zero initialization” puisque la valeur par défaut sera 0 ou équivalent).

int const i {}; // construction par défaut

Copiable par construction (CopyConstructible) et par affectation (CopyAssignable). Cela signifie que l'on peut créer une valeur en copiant une autre valeur. Par exemple, pour un entier :

int const i { 123 };
int j { i }; // construction par copie
j = i;       // affectation par copie

Comparable par égalité (EqualityComparable) ou par “plus petit que” (LessThanComparable). Cela signifie que l'on peut utiliser les opérateurs d'égalité == et “plut petit que” < (ainsi que les opérateurs dérivés : “différent de” !=, “plus petit ou égal à” <=, “plus grand que” > et “plus grand ou égal à” >=) :

int const i { 123 };
int const j { 456 };
cout << (i == j) << endl; // égalité
cout << (i != j) << endl; // différent
cout << (i < j) << endl;  // plus petit
cout << (i <= j) << endl; // plus petit ou égal
cout << (i > j) << endl;  // plus grand
cout << (i >= j) << endl; // plus grand ou égal

Si cela a un sens, la sémantique de valeur permet également de définir des opérateurs arithmétiques classiques : addition +, soustraction -, multiplication * et division /. Par exemple, pour les entiers :

int const i { 123 };
int const j { 456 };
cout << (i + j) << endl; // addition
cout << (i - j) << endl; // soustraction
cout << (i * j) << endl; // multiplication
cout << (i / j) << endl; // division

Le rôle de ces opérateurs peut varier en fonction du type. Par exemple, l'opérateur + correspondra à une addition pour les types numériques et à une concaténation pour les chaînes de caractères string.

main.cpp
#include <iostream>
#include <string>
 
int main() {
    int const i1 { 1 };
    int const i2 { 2 };
    std::cout << (i1 + i2) << std::endl; // addition
    std::string const s1 { "1" };
    std::string const s2 { "2" };
    std::cout << (s1 + s2) << std::endl; // concaténation
}

affiche :

3
12

De plus, selon les types, tous les opérateurs n'ont pas forcement un sens. Par exemple, pour les chaînes string, seul l'opérateur + a un sens, les autres opérateurs ne sont pas définis. Pour les collections (vector, array, etc.), l'opérateur + n'est pas défini (c'est un choix des concepteurs, ils auraient pu définir l'opérateur + pour fussionner deux collections, mais pour des raisons de sémantique, ce n'est pas le cas).

Les deux grands types de classes sont les classes à sémantique de valeur (que vous avez vu dans ce chapitre) et les classes à sémantique d'entité. Vous apprendrez dans la partie “programmation orientée objet” comment créer ces types de classes. Pour les distinguer, le plus simple est de se poser des questions sur ce que peut faire une classe ou non.

  • Est-ce que cela à un sens de pouvoir copier un objet ?
  • Est-ce que cela à un sens de tester si deux objets sont égaux ?
  • Est-ce que cela à un sens d’additionner deux objets ?

La majorité des classes de la bibliothèque standard sont à sémantique de valeur. Vous verrez dans la partie sur la programmation objet comment identifier et créer des classes à sémantique d'entité.

Les algorithmes d'égalité

Si vous souhaitez comparer deux sous-ensembles de collections ou si vous souhaitez utiliser un prédicat différent, il ne sera pas possible d'utiliser l'opérateur d'égalité ==. Dans ce cas, la bibliothèque standard fournit l'algorithme std::equal pour comparer si deux collections sont identiques ou non.

Lorsque l'on découvre une nouvelle fonction, la première chose à faire est de jetter un coup d'oeil sur la documentation : std::equal. Cette page nous apprend plusieurs choses :

  • cet algorithme est défini dans le fichier d'en-tête <algorithm>, il faudra donc l'inclure dans votre code pour utiliser equal ;
  • il existe quatre versions de cet algorithme :
    • le premier prend en argument le début et la fin d'une première collection et le début d'une seconde collection ;
    • le second est similaire au premier, avec un prédicat personnalisé ;
    • la troisième prend en argument le début et la fin de la première collection, puis de la seconde ;
    • le quatrième est similaire au troisième, avec un prédicat personnalisé.
  • le prédicat doit respecter la signature suivante : bool pred(const Type1 &a, const Type2 &b); (c'est donc une fonction binaire - une fonction qui prend deux arguments - et retourne un booléen).

La page de documentation donne également des codes d'exemple d'utilisation de ces fonctions.

La première (et deuxième) version de equal doit être utilisé avec précaution. Si on compare par exemple deux collections de tailles différentes (par exemple les chaînes “abcd” et “abcdEF”), la comparaison se terminera lorsque l'algorithme atteint la fin de la première collection donnée en argument. Dans ce cas, les deux chaînes seront considérées comme égale, alors que ce sont simplement les premiers caractères de la première chaîne qui sont retrouvé dans la seconde.

La troisième (et quatrième) version de equal permet de tester si l'algorithme est arrivé à la fin des deux collections et donc qu'elle sont parfaitement identiques. Voici un code d'exemple pour illustrer ce point :

#include <iostream>
#include <string>
#include <algorithm>
 
int main() {
    std::string const s1 { "abcd" };
    std::string const s2 { "abcdEF" };
    std::cout << std::boolalpha;
    std::cout << std::equal(begin(s1), end(s1), begin(s2)) << std::endl;
    std::cout << std::equal(begin(s2), end(s2), begin(s1)) << std::endl;
    std::cout << std::endl;
    std::cout << std::equal(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::equal(begin(s2), end(s2), begin(s1), end(s1)) << std::endl;
}

affiche :

true
false

false
false

Par défaut, préférez l'utilisation de la version prenant le début et la fin des deux collections (les versions 3 et 4 de std::equal).

Attention aux éléments que vous passez en argument dans les fonctions. Le compilateur vérifie que vous passer des collections de types identiques en argument, pas que les informations passées ont un sens. Par exemple, si vous passer en argument un élément d'une collection puis un élément d'une seconde collection, cela n'a pas de sens de parcourir une collection entre ce deux éléments.

std::equal(begin(s1), end(s2), begin(s2)); // erreur, les deux premiers arguments
                                           // ne proviennent pas de la même collection

Dans ce cas, ce code ne produit pas d'erreur de compilation, mais un comportement indéterminé (undefined behavior, UB). Ce type d'erreur est assez complexe à détecter et à corriger, il faut être très attentif pour les éviter.

Comparer des collections différentes

Comme cela a été dit auparavant, la classe string peut être vu comme une collection de caractères (char). Cependant, si on essaie de comparer une variable de type string à une tableau de caractères, on obtient une erreur :

main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
 
int main() {
    std::string const s { "abcdef" };
    std::vector<char> const a { 'a', 'b', 'c', 'd', 'e', 'f' };
    std::cout << std::boolalpha;
    std::cout << (s == a) << std::endl;
}

affiche l'erreur à la compilation suivante :

main.cpp:10:21: error: invalid operands to binary expression ('const std::string' 
(aka 'const basic_string<char, char_traits<char>, allocator<char> >') and 
'const std::vector<char>')
    std::cout << (s == a) << std::endl;
                  ~ ^  ~
1 error generated.

Cette erreur signifie que le compilateur ne trouve pas d'opérateur d'égalité == permettant de comparer un type string avec un type vector<char>. En effet, les opérateurs de comparaison sont définis pour accepter deux arguments de même type, par exemple deux string ou deux vector<char>, mais pas deux collection de type différents, même si ces deux types correspondent tous deux à des collections de char (on parle parfois de typage “fort” du C++).

Dans cette situation, il est possible d'utiliser la fonction equal pour tester l'égalité de deux collections. Cet algorithme fonctionne sur des collections de types différents, à partir du moment où les éléments sont comparable par égalité. Par exemple, il sera possible de comparer des string et des vector<char> (les éléments sont dans les deux cas des char) ou des vector<int> et vector<float> (il est possible de comparer un entier et un nombre réel), mais pas vector<int> et vector<string> (int et string ne sont pas comparable par égalité).

En utilisant std::equal, la comparaison devient :

main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
 
int main() {
    std::string const s { "abcdef" };
    std::vector<char> const v1 { 'a', 'b', 'c', 'd', 'e', 'f' };
    std::vector<char> const v2 { 'a', 'z', 'e', 'r', 't', 'y' };
    std::cout << std::boolalpha;
    std::cout << std::equal(begin(s), end(s), begin(v1), end(v1)) << std::endl;
    std::cout << std::equal(begin(s), end(s), begin(v2), end(v2)) << std::endl;
}

affiche :

true
false

Comparer des parties de collections

Avec equal, il est possible de comparer un partie d'une collection. Pour cela, il suffit de fournir des positions différentes que le début et la fin d'une collection. Par exemple, vous pouvez incrémenter ou décrémenter les positions de début (avec begin(s)+n) et de fin (avec end(s)-n) en faisant attention de ne pas donner des valeurs en dehors de la collection ou en utilisant des fonctions de recherche (find, que vous verrez dans un prochain chapitre).

Par exemple, pour comparer les quatre premiers éléments de deux collections, vous pouvez écrire :

main.cpp
#include <iostream>
#include <string>
#include <algorithm>
 
int main() {
    std::string const s1 { "abcdef" };
    std::string const s2 { "abcdEF" };
    std::cout << std::boolalpha;
    std::cout << std::equal(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::equal(begin(s1), begin(s1)+4, begin(s2), begin(s2)+4) << std::endl;
}

affiche :

false
true

La première version compare la totalité des deux chaînes (“abcdef” et “abcdEF”) et retourne false. La seconde version compare le début de la première chaîne (à partir de begin(s1) donc “a” jusqu'au quatrième caractère begin(s1)+4 donc “d”) avec le début de la seconde (également “abcd”) et retourne true.

Attention en utilisant la notation begin(s)+n, si vous sortez de la collection, cela produit une comportement indéterminé (undefined behavior), donc aucun message d'erreur vous prévenant qu'il y a un problème.

Utiliser un prédicat personnalisé

Lorsque les collections contiennent des éléments de types non comparables ou lorsque la comparaison d'égalité par défaut ne convient pas, il est possible de fournir un prédicat personnalisé dans std::equal. Par exemple, si vous souhaitez comparer deux chaînes de caractères, sans prendre en compte la casse (c'est-à-dire sans prendre en compte les majuscules ou minuscules), il n'est pas possible d'utiliser l'opérateur == classique.

La solution dans ce cas est d'utiliser la fonction de conversion std::toupper (ou std::tolower) pour convertir tous les caractères en majuscule pour faire la comparaison. Pour écrire ce prédicat, le plus simple dans ce cas est d'écrire une fonction lambda.

Cette fonction lambda doit faire la comparaison entre deux éléments et retourner un booléen. La signature sera la même que cela que vous avez déjà vu :

[](auto lhs, auto rhs){ return expression_booléenne_avec_lhs_et_rhs; }

Les paramètres lhs et rhs sont les éléments des collections (des caractères dans cet exemple) qui sont comparés deux à deux. Pour convertir ces deux valeurs en majuscule, il faudra donc écrire : std::toupper(lhs) et std::toupper(rhs). Au final, la comparaison s'écrit :

return std::toupper(lhs) == std::toupper(rhs);

Le code complet pour la comparaison est (ne pas oublier d'inclure le fichier d'en-tête <cctype> pour utiliser toupper) :

#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
 
int main() {
    std::string const s1 { "abcdef" };
    std::string const s2 { "abcdEF" };
    std::cout << std::boolalpha;
    std::cout << std::equal(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::equal(begin(s1), end(s1), begin(s2), end(s2),
        [](auto lhs, auto rhs){ return std::toupper(lhs) == std::toupper(rhs); }) 
        << std::endl;
}

affiche :

false
true

La première version prend en compte la casse et retourne false alors que la seconde version ne la prend pas en compte et considère que les deux chaînes sont identiques.

L'ordre lexicographique

Comme indiqué précédemment, la sémantique de valeur autorise l'utilisation des opérateurs de comparaison habituels : < (plus petit que), (plus petit ou égal), > (plus grand que) et >= (plus grand ou égal). Les collections de la bibliothèque standard respectent la sémantique de valeur, il n'est donc pas étonnant que l'on puisse utiliser ces opérateurs avec les classes comme vector ou string. Mais que signifie ces comparaisons dans le cas de collections ?

Comme indiqué dans la documentation (par exemple pour string : “lexicographically compares two strings”), les opérateurs de comparaison utilise l'ordre lexicographique. Cet ordre n'est pas compliqué à comprendre, c'est l'ordre que l'on utilise pour ranger les mots dans un dictionnaire par exemple.

Pour comparaison deux collections, on commence par prendre le premier élément de chaque collection et on les compare. Si l'un des éléments d'une des chaînes est inférieur à l'élément de l'autre collection, la collection correspondante est inférieure à l'autre. Si les deux éléments sont égaux, on compare les éléments suivants de chaque collection. Si une collection se termine avant l'autre, elle est inférieure. Si tous les éléments sont identiques, les collections sont égales.

Voyons quelques exemples pour bien comprendre. Comparons les chaînes “abc” et “acd”. Les premiers caractères sont “a” et “a”. Ils sont identiques, on passe aux deuxièmes caractères. Ceux-ci sont “b” et “c”. Comme “b” est plus petit que “c”, la chaîne “abc” est inférieure à la chaîne “acd”.

Autre exemple. Comparons “abc” et ab”. Les premiers et deuxièmes caractères sont identiques, il faudrait donc comparer les troisièmes caractères. Cependant, la seconde chaîne ne possède pas de troisième caractère, elle est donc inférieure à la première.

De même avec une collection d'entiers. On aura par exemple la collection { 1, 2, 3 } qui sera inférieure à la collection { 1, 2, 4 } et supérieure à la collection { 1, 2 }.

main.cpp
#include <iostream>
#include <string>
 
int main() {
    std::cout << std::boolalpha;
    std::cout << (std::string { "abc" } < std::string { "acd" }) << std::endl;
    std::cout << (std::string { "abc" } < std::string { "ab" }) << std::endl;
    std::cout << std::endl;
    std::cout << (std::vector<int> { 1, 2, 3 } < std::vector<int> { 1, 2, 4 }) << std::endl;
    std::cout << (std::vector<int> { 1, 2, 3 } < std::vector<int> { 1, 2 }) << std::endl;
}

affiche :

true
false

true
false

La comparaison “plus petit que” est également un concept (LessThanComparable), ce qui implique que différentes propriétés doivent être respectées :

  • identité : n'importe quelle collection n'est pas inférieure à elle-même (elle est égale à elle-même) ;
  • inverse : si une collection est inférieur à une seconde collection, on peut également dire que la seconde collection n'est pas inférieure à la première (elle est supérieure ou égale) ;
  • transitivité : si une chaîne a est inférieur à une chaîne b et que cette chaîne b est inférieure à une chaîne c, alors la chaîne a est inférieure à la chaîne c.

Bien sûr, ce concept est valable également pour les autres types du C++ (int, float, etc.) et de la bibliothèque standard (string, complex, etc.) qui possèdent des opérateurs de comparaison. Lorsqu'une classe définie un opérateur de comparaison, il est logique aussi que les autres opérateurs soient définies (il faudrait que cela ait un sens de ne pas les définir). Vous apprendrez dans la partie sur la programmation orientée objet (en particulier dans la partie sur la sémantique de valeur) comment définir ces opérateurs dans une classe que vous créez.

Les algorithmes de comparaison

La fonction lexicographical_compare prend comme arguments le début et la fin des deux collections que l'on souhaite comparer. Elle existe en deux versions, avec ou sans prédicat personnalisé, et retourne vrai si la première collection est inférieure à la seconde.

main.cpp
#include <iostream>
#include <string>
#include <algorithm>
 
int main() {
    std::string const s1 { "azerty" };
    std::string const s2 { "abcdef" };
    std::string const s3 { "qwerty" };
    std::cout << std::boolalpha;
    std::cout << std::lexicographical_compare(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::lexicographical_compare(begin(s1), end(s1), begin(s3), end(s3)) << std::endl;
}

affiche :

true
false

Comme pour la fonction std::equal, on va pouvoir utiliser la fonction lexicographical_compare pour (voir les exercices) :

  • tester des collections différentes ;
  • comparer des parties de collections ;
  • utiliser un prédicat personnalisé.

Exercices

  • comparer vector<int> et vector<float>
  • comparer vector<int> et vector<string> (aide : utilise std::stoi)
  • comparer vector<int> et vector<string> (qui contient “un”, “deux”, etc.)
  • tester si palindrome (un palindrome est un mot qui peut être lu de droite à gauche ou de gauche à droite, comme par exemple “kayak” ou “radar”).
main.cpp
#include <iostream>
#include <string>
#include <algorithm>
 
int main() {
    std::string const s1 { "azerty" }; // non palindromique
    std::string const s2 { "abccba" }; // palindromique
    std::cout << std::boolalpha;
    std::cout << std::equal(begin(s1), end(s1), s1.rbegin()) << std::endl;
    std::cout << std::equal(begin(s2), end(s2), s2.rbegin()) << std::endl;
}

affiche :

false
true
  • Tester des collections différentes
main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
 
int main() {
    std::string const s { "abcdef" };
    std::vector<char> const a { 'a', 'z', 'e', 'r', 't', 'y' };
    std::cout << std::boolalpha;
    std::cout << std::lexicographical_compare(begin(s), end(s), begin(a), end(a)) << std::endl;
}

affiche :

true
  • Comparer des parties de collections
main.cpp
#include <iostream>
#include <string>
#include <algorithm>
 
int main() {
    std::string const s1 { "abcdef" };
    std::string const s2 { "abcdfg" };
    std::cout << std::boolalpha;
    std::cout << std::lexicographical_compare(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::lexicographical_compare(begin(s1), begin(s1)+4, begin(s2), begin(s2)+4) << std::endl;
}

affiche :

true
false
  • Utiliser un prédicat personnalisé
main.cpp
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
 
int main() {
    std::string const s1 { "ABCDEF" };
    std::string const s2 { "azerty" };
    std::cout << std::boolalpha;
    std::cout << std::lexicographical_compare(begin(s1), end(s1), begin(s2), end(s2)) << std::endl;
    std::cout << std::equal(begin(s1), end(s1), begin(s2), end(s2),
        [](auto lhs, auto rhs){ return std::toupper(lhs) < std::toupper(rhs); }) 
        << std::endl;
}

affiche :

true
false
Chapitre précédent Sommaire principal Chapitre suivant
comparer.1431186157.txt.gz · Dernière modification: 2015/05/09 17:42 par 78.240.244.130