Ceci est une ancienne révision du document !
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|
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.
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 :
#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 :
#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).
#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 :
a
, a == a
est toujours vrai ;a == b
, alors b == a
;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.)
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
.
#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).
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é.
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 :
<algorithm>
, il faudra donc l'inclure dans votre code pour utiliser equal
;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
).
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.
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 :
#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 :
#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
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 :
#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.
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.
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 }
.
#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 :
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.
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.
#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) :
#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
#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
#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
#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 |
---|