Opérations de comparaison en C ++ 20

La réunion à Cologne est passée, la norme C ++ 20 a été réduite à un aspect plus ou moins fini (au moins jusqu'à l'apparition de notes spéciales), et je voudrais parler d'une des innovations à venir. Il s'agit d'un mécanisme qui est généralement appelé opérateur <=> (la norme le définit comme un "opérateur de comparaison à trois voies", mais il a le surnom informel de "vaisseau spatial"), mais je pense que sa portée est beaucoup plus large.

Nous n'aurons pas seulement un nouvel opérateur - la sémantique des comparaisons subira des changements importants au niveau du langage lui-même.

Même si vous ne pouvez rien retirer d'autre de cet article, n'oubliez pas ce tableau:
Égalité
Rationalisation
Basique
==
<=>
Dérivés
! =
< , > , <= , > =

Nous allons maintenant avoir un nouvel opérateur, <=> , mais, plus important encore, les opérateurs sont maintenant systématisés. Il existe des opérateurs de base et des opérateurs dérivés - chaque groupe a ses propres capacités.

Nous parlerons brièvement de ces fonctionnalités dans l'introduction et examinerons plus en détail dans les sections suivantes.

Les opérateurs de base peuvent être inversés (c'est-à-dire réécrits avec l'ordre inverse des paramètres). Les instructions dérivées peuvent être réécrites via l'instruction de base correspondante. Ni les candidats convertis ni réécrits ne génèrent de nouvelles fonctions, ils sont simplement des remplacements au niveau du code source et sont sélectionnés à partir d'un ensemble étendu de candidats . Par exemple, l'expression a <9 peut maintenant être évaluée comme a.operator <=> (9) <0 et l'expression 10! = B as ! Operator == (b, 10) . Cela signifie qu'il sera possible de se passer d'un ou deux opérateurs où, pour obtenir le même comportement, il est désormais nécessaire d'écrire manuellement 2, 4, 6, voire 12 opérateurs. Un bref aperçu des règles sera présenté ci-dessous avec un tableau de toutes les transformations possibles.

Les opérateurs de base et dérivés peuvent être définis par défaut . Dans le cas des opérateurs de base, cela signifie que l'opérateur sera appliqué à chaque membre dans l'ordre de déclaration; dans le cas des opérateurs dérivés, les candidats réécrits seront utilisés.

Il convient de noter qu'il n'y a pas une telle transformation dans laquelle un opérateur d'un type (c'est-à-dire l'égalité ou l'ordre) pourrait être exprimé par un opérateur d'un autre type. En d'autres termes, les colonnes de notre tableau ne dépendent en aucune manière les unes des autres. L'expression a == b ne sera jamais évaluée comme opérateur <=> (a, b) == 0 implicitement (mais, bien sûr, rien ne vous empêche de définir votre opérateur == en utilisant l' opérateur <=> si vous le souhaitez).

Prenons un petit exemple dans lequel nous montrons à quoi ressemble le code avant et après l'application de la nouvelle fonctionnalité. Nous allons écrire un type de chaîne non sensible à la casse, CIString , dont les objets peuvent être comparés à la fois entre eux et avec char const * .

En C ++ 17, pour notre tâche, nous devons écrire 18 fonctions de comparaison:

class CIString { string s; public: friend bool operator==(const CIString& a, const CIString& b) { return assize() == bssize() && ci_compare(asc_str(), bsc_str()) == 0; } friend bool operator< (const CIString& a, const CIString& b) { return ci_compare(asc_str(), bsc_str()) < 0; } friend bool operator!=(const CIString& a, const CIString& b) { return !(a == b); } friend bool operator> (const CIString& a, const CIString& b) { return b < a; } friend bool operator>=(const CIString& a, const CIString& b) { return !(a < b); } friend bool operator<=(const CIString& a, const CIString& b) { return !(b < a); } friend bool operator==(const CIString& a, const char* b) { return ci_compare(asc_str(), b) == 0; } friend bool operator< (const CIString& a, const char* b) { return ci_compare(asc_str(), b) < 0; } friend bool operator!=(const CIString& a, const char* b) { return !(a == b); } friend bool operator> (const CIString& a, const char* b) { return b < a; } friend bool operator>=(const CIString& a, const char* b) { return !(a < b); } friend bool operator<=(const CIString& a, const char* b) { return !(b < a); } friend bool operator==(const char* a, const CIString& b) { return ci_compare(a, bsc_str()) == 0; } friend bool operator< (const char* a, const CIString& b) { return ci_compare(a, bsc_str()) < 0; } friend bool operator!=(const char* a, const CIString& b) { return !(a == b); } friend bool operator> (const char* a, const CIString& b) { return b < a; } friend bool operator>=(const char* a, const CIString& b) { return !(a < b); } friend bool operator<=(const char* a, const CIString& b) { return !(b < a); } }; 

En C ++ 20, vous ne pouvez effectuer que 4 fonctions:

 class CIString { string s; public: bool operator==(const CIString& b) const { return s.size() == bssize() && ci_compare(s.c_str(), bsc_str()) == 0; } std::weak_ordering operator<=>(const CIString& b) const { return ci_compare(s.c_str(), bsc_str()) <=> 0; } bool operator==(char const* b) const { return ci_compare(s.c_str(), b) == 0; } std::weak_ordering operator<=>(const char* b) const { return ci_compare(s.c_str(), b) <=> 0; } }; 

Je vais vous dire ce que tout cela signifie, plus en détail, mais d'abord, revenons un peu en arrière et rappelons-nous comment les comparaisons ont fonctionné avec la norme C ++ 20.

Comparaisons dans les normes de C ++ 98 à C ++ 17


Les opérations de comparaison n'ont pas beaucoup changé depuis la création du langage. Nous avions six opérateurs: == ,! = , < , > , <= Et > = . La norme définit chacun d'eux pour les types intégrés, mais en général, ils obéissent aux mêmes règles. Lors de l'évaluation d' une expression a @ b (où @ est l'un des six opérateurs de comparaison), le compilateur recherche les fonctions membres, les fonctions libres et les candidats intégrés nommés operator @ , qui peuvent être appelés avec le type A ou B dans l'ordre spécifié. Le candidat le plus approprié est sélectionné parmi eux. C’est tout. En fait, tous les opérateurs fonctionnaient de la même manière: l'opération < ne différait pas de << .

Un ensemble de règles aussi simple est facile à apprendre. Tous les opérateurs sont absolument indépendants et équivalents. Peu importe ce que nous, humains, savons de la relation fondamentale entre les opérations == et ! = . En termes de langue, c'est la même chose. Nous utilisons des idiomes. Par exemple, nous définissons l'opérateur ! = Through == :

 bool operator==(A const&, A const&); bool operator!=(A const& lhs, A const& rhs) { return !(lhs == rhs); } 

De même, par l'opérateur < nous définissons tous les autres opérateurs de relation. Nous utilisons ces idiomes car, malgré les règles du langage, nous ne considérons pas vraiment les six opérateurs comme équivalents. Nous acceptons que deux d'entre eux sont basiques ( == et < ), et à travers eux tous les autres sont déjà exprimés.

En fait, la bibliothèque de modèles standard est entièrement construite sur ces deux opérateurs, et le grand nombre de types dans le code exploité contient des définitions d'un seul d'entre eux ou des deux.

Cependant, l'opérateur < n'est pas très approprié pour le rôle de base pour deux raisons.

Premièrement, il n'est pas garanti que d'autres opérateurs de relations s'expriment à travers elle. Oui, a> b signifie exactement la même chose que b <a , mais il n'est pas vrai que a <= b signifie exactement la même chose que ! (B <a) . Les deux dernières expressions seront équivalentes s'il existe une propriété de trichotomie dans laquelle, pour deux valeurs quelconques, une seule des trois affirmations est vraie: a <b , a == b ou a> b . S'il y a une trichotomie, l'expression a <= b signifie que nous avons affaire au premier ou au deuxième cas ... et cela équivaut à l'affirmation selon laquelle nous n'avons pas affaire au troisième cas. Par conséquent (a <= b) ==! (A> b) ==! (B <a) .

Mais que faire si l'attitude n'a pas la propriété de la trichotomie? Ceci est caractéristique des relations d'ordre partiel. Un exemple classique est les nombres à virgule flottante pour lesquels l'une des opérations 1.f <NaN , 1.f == NaN et 1.f> NaN donne false . Par conséquent, 1.f <= NaN donne également un mensonge , mais en même temps ! (NaN <1.f) est vrai .

La seule façon d'implémenter l'opérateur <= en termes généraux via les opérateurs de base est de peindre les deux opérations comme (a == b) || (a <b) , ce qui est un grand pas en arrière si nous devons encore faire face à l'ordre linéaire, car alors aucune fonction ne sera appelée, mais deux (par exemple, l'expression «abc..xyz9» <= «abc ..xyz1 " devra être réécrit comme (" abc..xyz9 "==" abc..xyz1 ") || (" abc..xyz9 "<" abc..xyz1 ") et deux fois pour comparer la ligne entière).

Deuxièmement, l'opérateur < ne convient pas très bien au rôle de base en raison des particularités de son utilisation dans les comparaisons lexicographiques. Les programmeurs font souvent cette erreur:

 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } }; 

Pour définir l'opérateur == pour une collection d'éléments, il suffit d'appliquer == à chaque membre une fois, mais cela ne fonctionnera pas avec l'opérateur < . Du point de vue de cette implémentation, les ensembles A {1, 2} et A {2, 1} seront considérés comme équivalents (car aucun d'entre eux n'est inférieur à l'autre). Pour résoudre ce problème, appliquez l'opérateur < deux fois à chaque membre, sauf le dernier:

 bool operator< (A const& rhs) const { if (t < rhs.t) return true; if (rhs.t < t) return false; return u < rhs.u; } 

Enfin, pour garantir le bon fonctionnement des comparaisons d'objets hétérogènes - c'est-à-dire pour s'assurer que les expressions a == 10 et 10 == a signifient la même chose - ils recommandent généralement d'écrire des comparaisons en tant que fonctions libres. En fait, c'est généralement le seul moyen de mettre en œuvre de telles comparaisons. Cela n'est pas pratique car, d'une part, vous devez surveiller le respect de cette recommandation, et d'autre part, vous devez généralement déclarer ces fonctions amis cachés pour une implémentation plus pratique (c'est-à-dire à l'intérieur du corps de classe).

Notez que lors de la comparaison d'objets de différents types, il n'est pas toujours nécessaire d'écrire l' opérateur == (X, int) ; ils peuvent également signifier des cas où int peut être transtypé implicitement en X.

Résumons les règles au standard C ++ 20:

  • Toutes les instructions sont traitées de la même manière.
  • Nous utilisons des idiomes pour faciliter la mise en œuvre. Les opérateurs == et < nous prenons pour les idiomes de base et exprimons les opérateurs de relation restants à travers eux.
  • C'est juste que l'opérateur <n'est pas très adapté au rôle de la base.
  • Il est important (et recommandé) d'écrire des comparaisons d'objets hétérogènes en tant que fonctions libres.

Nouvel opérateur de commande de base: <=>


Le changement le plus significatif et le plus notable dans le travail de comparaison en C ++ 20 est l'ajout d'un nouvel opérateur - opérateur <=> , un opérateur de comparaison à trois voies.

Nous connaissons déjà les comparaisons tripartites par les fonctions memcmp / strcmp en C et basic_string :: compare () en C ++. Ils renvoient tous une valeur de type int , qui est représentée par un nombre positif arbitraire si le premier argument est supérieur au second, 0 s'ils sont égaux et un nombre négatif arbitraire sinon.

L'opérateur «vaisseau spatial» ne renvoie pas une valeur int , mais un objet appartenant à l'une des catégories de comparaison, dont la valeur reflète le type de relation entre les objets comparés. Il existe trois catégories principales:

  • strong_ordering : une relation d'ordre linéaire dans laquelle l'égalité implique l'interchangeabilité des éléments (c'est-à-dire (a <=> b) == strong_ordering :: equal implique que f (a) == f (b) est valable pour toutes les fonctions appropriées f Le terme «fonction appropriée» n'a intentionnellement pas de définition claire, mais celles-ci n'incluent pas les fonctions qui renvoient les adresses de leurs arguments ou la capacité () du vecteur, etc. Nous ne nous intéressons qu'aux propriétés «essentielles», qui sont également très vagues, mais conditionnellement possibles supposons que nous parlons de la valeur du type. La valeur du vecteur y est contenue m éléments, mais pas son adresse, etc.). Cette catégorie comprend les valeurs suivantes: strong_ordering :: supérieur , strong_ordering :: égal et strong_ordering :: less .
  • faible_ordre : relation d'ordre linéaire dans laquelle l'égalité ne définit qu'une certaine classe d'équivalence. Un exemple classique est la comparaison de chaînes insensible à la casse, lorsque deux objets peuvent être low_ordering :: equivalent , mais ne sont pas strictement égaux (cela explique le remplacement du mot égal par équivalent dans le nom de la valeur).
  • partial_ordering : relation d'ordre partiel. Dans cette catégorie, une valeur supplémentaire est ajoutée aux valeurs supérieures , équivalentes et inférieures (comme dans faible_ordre ) - non classées ("désordonnées"). Il peut être utilisé pour exprimer des relations d'ordre partiel dans un système de types: 1.f <=> NaN donne la valeur partial_ordering :: unordered .

Vous travaillerez principalement avec la catégorie strong_ordering ; Il s'agit également de la catégorie optimale à utiliser par défaut. Par exemple, 2 <=> 4 renvoie strong_ordering :: less et 3 <=> -1 renvoie strong_ordering :: Greater .

Les catégories d'un ordre supérieur peuvent être implicitement réduites à des catégories d'un ordre plus faible (c'est-à-dire que strong_ordering est réductible à faible_ordering ). Dans ce cas, le type de relation actuel est conservé (c'est-à-dire que strong_ordering :: equal se transforme en faiblesse_ordering :: equivalent ).

Les valeurs des catégories de comparaison peuvent être comparées avec le littéral 0 (pas avec un int et pas avec un int égal à 0 , mais simplement avec le littéral 0 ) en utilisant l'un des six opérateurs de comparaison:

 strong_ordering::less < 0 // true strong_ordering::less == 0 // false strong_ordering::less != 0 // true strong_ordering::greater >= 0 // true partial_ordering::less < 0 // true partial_ordering::greater > 0 // true // unordered -  ,   //       partial_ordering::unordered < 0 // false partial_ordering::unordered == 0 // false partial_ordering::unordered > 0 // false 

C'est grâce à une comparaison avec le littéral 0 que l' on peut implémenter les opérateurs de relation: a @ b est équivalent à (a <=> b) @ 0 pour chacun de ces opérateurs.

Par exemple, 2 <4 peut être calculé comme (2 <=> 4) <0 , ce qui se transforme en strong_ordering :: less <0 et donne la valeur true .

L'opérateur <=> correspond bien mieux au rôle de l'élément de base que l'opérateur < , car il élimine les deux problèmes de ce dernier.

Premièrement, l'expression a <= b est garantie d'être équivalente à (a <=> b) <= 0 même avec un ordre partiel. Pour deux valeurs non ordonnées, a <=> b donnera la valeur partial_ordered :: unordered et partial_ordered :: unordered <= 0 donnera false , ce dont nous avons besoin. Cela est possible car <=> peut renvoyer plus de variétés de valeurs: par exemple, la catégorie partial_ordering contient quatre valeurs possibles. Une valeur de type bool ne peut être vraie ou fausse , donc avant, nous ne pouvions pas distinguer les comparaisons de valeurs ordonnées et non ordonnées.

Pour plus de clarté, considérons un exemple de relation d'ordre partiel qui n'est pas liée aux nombres à virgule flottante. Supposons que nous voulons ajouter un état NaN à un type int , où NaN est juste une valeur qui ne forme pas une paire ordonnée avec une valeur impliquée. Vous pouvez le faire en utilisant std :: facultatif pour le stocker:

 struct IntNan { std::optional<int> val = std::nullopt; bool operator==(IntNan const& rhs) const { if (!val || !rhs.val) { return false; } return *val == *rhs.val; } partial_ordering operator<=>(IntNan const& rhs) const { if (!val || !rhs.val) { //  unordered   //     return partial_ordering::unordered; } // <=>   strong_ordering  int, //        partial_ordering return *val <=> *rhs.val; } }; IntNan{2} <=> IntNan{4}; // partial_ordering::less IntNan{2} <=> IntNan{}; // partial_ordering::unordered //     .    IntNan{2} < IntNan{4}; // true IntNan{2} < IntNan{}; // false IntNan{2} == IntNan{}; // false IntNan{2} <= IntNan{}; // false 

L'opérateur <= renvoie la valeur correcte car maintenant nous pouvons exprimer plus d'informations au niveau de la langue elle-même.

Deuxièmement, pour obtenir toutes les informations nécessaires, il suffit d’appliquer <=> une fois, ce qui facilite la mise en œuvre de la comparaison lexicographique:
 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } strong_ordering operator<=>(A const& rhs) const { //    //  t.   != 0 (..  t // ),    //   if (auto c = t <=> rhs.t; c != 0) return c; //     //    return u <=> rhs.u; }; 

Voir P0515 , la phrase originale pour ajouter l' opérateur <=>, pour une discussion plus détaillée .

Nouvelles fonctionnalités opérateur


Nous ne mettons pas seulement à notre disposition un nouvel opérateur. En fin de compte, si l'exemple montré ci-dessus avec la déclaration de structure A disait seulement qu'au lieu de x <y nous devons maintenant écrire (x <=> y) <0 à chaque fois, personne ne l'aimerait.

Le mécanisme de résolution des comparaisons en C ++ 20 diffère sensiblement de l'ancienne approche, mais ce changement est directement lié au nouveau concept de deux opérateurs de comparaison de base: == et <=> . Si auparavant c'était un idiome (enregistrement via == et < ), que nous avons utilisé, mais que le compilateur ne connaissait pas, maintenant il comprendra cette différence.

Encore une fois, je vais vous donner le tableau que vous avez déjà vu au début de l'article:
Égalité
Rationalisation
Basique
==
<=>
Dérivés
! =
< , > , <= , > =

Chacun des opérateurs de base et dérivés a reçu une nouvelle capacité, que je dirai quelques mots plus loin.

Inversion des opérateurs de base


Par exemple, prenez un type qui ne peut être comparé qu'à int :

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } }; 

Du point de vue des anciennes règles, il n'est pas surprenant que l'expression a == 10 fonctionne et s'évalue en a.operator == (10) .

Mais qu'en est-il de 10 == a ? En C ++ 17, cette expression serait considérée comme une erreur de syntaxe claire. Il n'y a pas un tel opérateur. Pour qu'un tel code fonctionne, vous devez écrire un opérateur symétrique == , qui prendrait d'abord la valeur de int , puis A ... et pour l'implémenter, cela devrait être sous la forme d'une fonction libre.

En C ++ 20, les opérateurs de base peuvent être inversés. Pour 10 == a, le compilateur trouvera un opérateur candidat == (A, int) (en fait, c'est une fonction membre, mais pour plus de clarté, je l'écris ici comme une fonction libre), puis en plus - une variante avec l'ordre inverse des paramètres, c'est-à-dire . opérateur == (int, A) . Ce deuxième candidat coïncide avec notre expression (et idéalement), nous allons donc le choisir. L'expression 10 == a en C ++ 20 est évaluée comme a.operator == (10) . Le compilateur comprend que l'égalité est symétrique.

Nous allons maintenant étendre notre type afin qu'il puisse être comparé à int non seulement via l'opérateur d'égalité, mais aussi via l'opérateur de commande:

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

Encore une fois, l'expression a <=> 42 fonctionne bien et est calculée selon les anciennes règles comme a.operator <=> (42) , mais 42 <=> a serait erroné du point de vue de C ++ 17, même si l'opérateur < => existait déjà dans la langue. Mais en C ++ 20, l' opérateur <=> , comme l' opérateur == , est symétrique: il reconnaît les candidats inversés. Pour 42 <=> a, un opérateur de fonction membre <=> (A, int) sera trouvé (encore une fois, je l'écris ici comme une fonction libre juste pour plus de clarté), ainsi qu'un opérateur candidat synthétique <=> (int, A) . Cette version inversée correspond exactement à notre expression - nous la sélectionnons.

Cependant, 42 <=> a n'est PAS calculé comme un opérateur <=> (42) . Ce serait faux. Cette expression est évaluée à 0 <=> a.operator <=> (42) . Essayez de comprendre pourquoi cette entrée est correcte.

Il est important de noter que le compilateur ne crée aucune nouvelle fonction. Lors du calcul de 10 == a , le nouvel opérateur opérateur == (int, A) n'apparaissait pas et lors du calcul de 42 <=> a , l' opérateur <=> (int, A) n'apparaissait pas. Seulement deux expressions sont réécrites par le biais de candidats inversés. Je le répète: aucune nouvelle fonction n'est créée.

Notez également qu'un enregistrement avec l'ordre inverse des paramètres n'est disponible que pour les opérateurs de base, mais pas pour les dérivés. Soit:

 struct B { bool operator!=(int) const; }; b != 42; // ok   C++17,   C++20 42 != b; //    C++17,   C++20 

Réécriture des opérateurs dérivés


Revenons à notre exemple avec la structure A :

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

Prenez l'expression a! = 17 . En C ++ 17, il s'agit d'une erreur de syntaxe car l' opérateur! = Operator n'existe pas. Cependant, en C ++ 20, pour les expressions contenant des opérateurs de comparaison de dérivés, le compilateur recherchera également les opérateurs de base correspondants et exprimera des comparaisons de dérivés à travers eux.

Nous savons qu'en mathématiques, l'opération ! = Signifie essentiellement NON == . Maintenant, cela est connu du compilateur. Pour l'expression a! = 17, il cherchera non seulement l' opérateur! = Opérateurs , mais aussi l' opérateur == (et, comme dans les exemples précédents, l' opérateur inversé == ). Pour cet exemple, nous avons trouvé un opérateur d'égalité qui nous convient presque - il suffit de le réécrire en fonction de la sémantique souhaitée: a! = 17 sera calculé comme ! (A == 17) .

De même, 17! = A est calculé comme ! A.operator == (17) , qui est à la fois une version réécrite et une version inversée.

Des transformations similaires sont également effectuées pour les opérateurs de commande. Si nous écrivions un <9 , nous essayions (sans succès) de trouver l' opérateur < , et considérions également les candidats de base: l' opérateur <=> . Le remplacement correspondant pour les opérateurs de relation ressemble à ceci: a @ b (où @ est l'un des opérateurs de relation) est calculé comme (a <=> b) @ 0 . Dans notre cas, un opérateur <=> (9) <0 . De même, 9 <= a est calculé comme 0 <= a.operator <=> (9) .

Notez que, comme dans le cas de l'appel, le compilateur ne crée aucune nouvelle fonction pour les candidats réécrits. Ils sont simplement calculés différemment et toutes les transformations sont effectuées uniquement au niveau du code source.

Ce qui précède m'amène aux conseils suivants:

OPÉRATEURS DE BASE UNIQUEMENT : Définissez uniquement les opérateurs de base (== et <=>) dans votre type.

Les opérateurs de base donnant l'ensemble des comparaisons, il suffit de les définir uniquement. Cela signifie que vous n'avez besoin que de 2 opérateurs pour comparer des objets du même type (au lieu de 6, à partir de maintenant) et de seulement 2 opérateurs pour comparer différents types d'objets (au lieu de 12). Si vous n'avez besoin que de l'opération d'égalité, écrivez simplement 1 fonction pour comparer des objets du même type (au lieu de 2) et 1 fonction pour comparer différents types d'objets (au lieu de 4). La classe std :: sub_match représente un cas extrême: en C ++ 17, elle utilise 42 opérateurs de comparaison, et en C ++ 20, elle n'en utilise que 8, tandis que la fonctionnalité ne souffre d'aucune façon.

Puisque le compilateur considère également les candidats inversés, tous ces opérateurs peuvent être implémentés en tant que fonctions membres. Vous n'avez plus besoin d'écrire des fonctions libres juste pour comparer des objets de différents types.

Règles spéciales pour trouver des candidats


Comme je l'ai déjà mentionné, la recherche de candidats pour un @ b en C ++ 17 a été réalisée selon le principe suivant: on trouve tous les opérateurs @ opérateur et on sélectionne parmi eux le plus approprié.

C ++ 20 utilise un ensemble étendu de candidats. Nous allons maintenant rechercher tous les opérateurs @ . Soit @@ l'opérateur de base de @ (il peut s'agir du même opérateur). On retrouve également tous les opérateurs @@ et pour chacun d'eux on ajoute sa version inversée. Parmi tous ces candidats trouvés, nous sélectionnons le plus adapté.

Notez que la surcharge de l'opérateur est autorisée en une seule passe. Nous n'essayons pas de remplacer différents candidats. D'abord, nous les collectons tous, puis choisissons le meilleur d'entre eux. Si cela n'existe pas, la recherche, comme précédemment, échoue.

Nous avons maintenant beaucoup plus de candidats potentiels, et donc plus d'incertitude. Prenons l'exemple suivant:

 struct C { bool operator==(C const&) const; bool operator!=(C const&) const; }; bool check(C x, C y) { return x != y; } 

En C ++ 17, nous n'avions qu'un seul candidat pour x! = Y , et maintenant il y en a trois: x.operator! = (Y),! X.operator == (y) et ! Y.operator == (x) . Que choisir? Ils sont tous pareils! (Remarque: le candidat y.operator! = (X) n'existe pas, car seuls les opérateurs de base peuvent être inversés .)

Deux règles supplémentaires ont été introduites pour lever cette incertitude. Les candidats non convertis sont préférables aux convertis; . , x.operator!=(y) «» !x.operator==(y) , «» !y.operator==(x) . , «» .

: operator@@ . . , .

-. — (, x < y , — (x <=> y) < 0 ), (, x <=> y void - , DSL), . . , bool ( : operator== bool , ?)

Par exemple:

 struct Base { friend bool operator<(const Base&, const Base&); // #1 friend bool operator==(const Base&, const Base&); }; struct Derived : Base { friend void operator<=>(const Derived&, const Derived&); // #2 }; bool f(Derived d1, Derived d2) { return d1 < d2; } 

d1 < d2 : #1 #2 . — #2 , , , . , d1 < d2 (d1 <=> d2) < 0 . , void 0 — , . , - , #1 .


, , C++17, . , - . :

  • ( )
  • ,
  • , .

, . .

. , , , , , ( ). , :

Option 1
Option 2
a == b
b == a

a != b
!(a == b)
!(b == a)
a <=> b
0 <=> (b <=> a)

a < b
(a <=> b) < 0
(b <=> a) > 0
a <= b
(a <=> b) <= 0
(b <=> a) >= 0
a > b
(a <=> b) > 0
(b <=> a) < 0
a >= b
(a <=> b) >= 0
(b <=> a) <= 0

« » , , .. a < b 0 < (b <=> a) , , , .


C++17 . . :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } bool operator!=(A const& rhs) const { return !(*this == rhs); } bool operator< (A const& rhs) const { //    ,     , //     ?:  &&/|| if (t < rhs.t) return true; if (rhs.t < t) return false; if (u < rhs.u) return true; if (rhs.u < u) return false; return v < rhs.v; } bool operator> (A const& rhs) const { return rhs < *this; } bool operator<=(A const& rhs) const { return !(rhs < *this); } bool operator>=(A const& rhs) const { return !(*this < rhs); } }; 

- std::tie() , .

, : :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } strong_ordering operator<=>(A const& rhs) const { //   T if (auto c = t <=> rhs.t; c != 0) return c; // ...  U if (auto c = u <=> rhs.u; c != 0) return c; // ...  V return v <=> rhs.v; } }; 

. <=> < . , . c != 0 , , ( ), .

. C++20 , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; strong_ordering operator<=>(A const& rhs) const = default; }; 

, . , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; auto operator<=>(A const& rhs) const = default; }; 

. , , :

 struct A { T t; U u; V v; auto operator<=>(A const& rhs) const = default; }; 

, , . : operator== , operator<=> .


C++20: . . , , , .


PVS-Studio , <=> . , -. , , (. " "). ++ .

PVS-Studio <, :

 bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } 

. , - . .

: Comparisons in C++20 .

Source: https://habr.com/ru/post/fr465575/


All Articles