Le nouvel opérateur de vaisseau spatial en C ++ 20

C ++ 20 ajoute un nouvel opérateur appelé «vaisseau spatial»: <=> . Il n'y a pas si longtemps, Simon Brand a publié un article contenant des informations conceptuelles détaillées sur ce qu'est cet opérateur et à quelles fins il est utilisé. La tâche principale de cet article est d'étudier les applications spécifiques du nouvel opérateur "étrange" et de son operator== analogique operator== , ainsi que de formuler des recommandations pour son utilisation dans le codage quotidien.


Comparaison


Il n'est pas rare de voir du code comme celui-ci:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  bool operator==(const IntWrapper& rhs) const { return value == rhs.value; }  bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs);    }  bool operator<(const IntWrapper& rhs)  const { return value < rhs.value;  }  bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this);    }  bool operator>(const IntWrapper& rhs)  const { return rhs < *this;        }  bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs);    } }; 

Remarque: les lecteurs attentifs remarqueront que cela est encore moins verbeux qu'il ne devrait l'être dans le code avant C ++ 20. Plus d'informations à ce sujet plus tard.

Vous devez écrire beaucoup de code standard pour vous assurer que notre type est comparable à quelque chose du même type. Ok, nous allons le découvrir pendant un certain temps. Vient ensuite quelqu'un qui écrit comme ceci:

 constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } int main() {  static_assert(is_lt(0, 1)); } 

La première chose que vous remarquerez est que le programme ne se compilera pas.

error C3615: constexpr function 'is_lt' cannot result in a constant expression

Le problème est que constexpr été oublié dans la fonction de comparaison. Ensuite, certains ajouteront constexpr à tous les opérateurs de comparaison. Quelques jours plus tard, quelqu'un ajoutera l' is_gt , mais notez que tous les opérateurs de comparaison n'ont pas de spécification d'exception, et vous devrez suivre le même processus fastidieux d'ajouter noexcept à chacune des 5 surcharges.

C'est là que le nouvel opérateur de vaisseau spatial C ++ 20 vient à notre aide. Voyons comment vous pouvez écrire le IntWrapper origine dans le monde C ++ 20:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default; }; 

La première différence que vous remarquerez peut-être est la nouvelle inclusion de <compare> . L'en-tête <compare> est chargé de remplir le compilateur avec tous les types de catégories de comparaison nécessaires à l'opérateur du vaisseau spatial, afin qu'il renvoie un type adapté à notre fonction par défaut. Dans l'extrait ci-dessus, le type de retour de auto sera std::strong_ordering .

Nous avons non seulement supprimé 5 lignes supplémentaires, mais nous n'avons même pas besoin de déterminer quoi que ce soit, le compilateur le fera pour nous. is_lt reste inchangé et fonctionne juste, tout en restant constexpr , bien que nous ne l'ayons pas spécifié explicitement dans notre operator<=> par défaut operator<=> . C'est bien, mais certaines personnes peuvent se is_lt pourquoi is_lt autorisé à compiler même s'il n'utilise pas du tout l'opérateur de vaisseau spatial. Trouvons la réponse à cette question.

Réécriture d'expressions


En C ++ 20, le compilateur est introduit dans un nouveau concept lié aux expressions «réécrites». L'opérateur du vaisseau spatial, avec l' operator== , est l'un des deux premiers candidats qui peuvent être réécrits. Pour un exemple plus spécifique d'expressions de réécriture, regardons l'exemple donné dans is_lt .

Tout en résolvant la surcharge, le compilateur choisira parmi un ensemble des candidats les plus appropriés, chacun correspondant à l'opérateur dont nous avons besoin. Le processus de sélection change très légèrement pour les opérations de comparaison et les opérations d'équivalence, lorsque le compilateur doit également collecter des candidats spéciaux transcrits et synthétisés ( [over.match.oper] /3.4 ).

Pour notre expression a < b norme stipule que nous pouvons rechercher des fonctions de type a pour l' operator<=> ou l' operator<=> qui acceptent ce type. C'est ce que fait le compilateur et découvre que le type a contient réellement IntWrapper::operator<=> . Le compilateur est alors autorisé à utiliser cet opérateur et à réécrire l'expression a < b as (a <=> b) < 0 . Cette expression réécrite est ensuite utilisée comme candidate à une résolution normale de surcharge.

Vous pouvez vous demander pourquoi cette expression réécrite est correcte. La justesse de l'expression découle en fait de la sémantique fournie par l'opérateur du vaisseau spatial. <=> est une comparaison à trois, ce qui implique que vous obtenez non seulement un résultat binaire, mais aussi un ordre (dans la plupart des cas). Si vous avez une commande, vous pouvez exprimer cette commande en termes d'opérations de comparaison. Un exemple rapide, l'expression 4 <=> 5 en C ++ 20 retournera le résultat std::strong_ordering::less . Le résultat de std::strong_ordering::less implique que 4 non seulement différent de 5 mais également strictement inférieur à cette valeur, ce qui rend l'application de l'opération (4 <=> 5) < 0 correcte et précise pour décrire notre résultat.

En utilisant les informations ci-dessus, le compilateur peut prendre n'importe quel opérateur de comparaison généralisé (c'est-à-dire < , > , etc.) et le réécrire en termes d'opérateur de vaisseau spatial. Dans la norme, une expression réécrite est souvent appelée (a <=> b) @ 0@ représente toute opération de comparaison.

Synthétiser des expressions


Les lecteurs ont peut-être remarqué une référence subtile aux expressions «synthétisées» ci-dessus, et ils jouent également un rôle dans ce processus de réécriture des déclarations. Considérez la fonction suivante:

 constexpr bool is_gt_42(const IntWrapper& a) {  return 42 < a; } 

Si nous utilisons notre définition d'origine pour IntWrapper , ce code ne sera pas compilé.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

Cela est logique avant C ++ 20, et la façon de résoudre ce problème consiste à ajouter des fonctions friend supplémentaires à IntWrapper qui occupent le côté gauche de l' int . Si vous essayez de créer cet exemple à l'aide du compilateur et de la IntWrapper C ++ 20, vous remarquerez peut-être que cela fonctionne à nouveau. Voyons pourquoi le code ci-dessus est toujours en cours de compilation en C ++ 20.

Tout en résolvant les surcharges, le compilateur collectera également ce que la norme appelle des candidats «synthétisés», ou une expression réécrite avec l'ordre inverse des paramètres. Dans l'exemple ci-dessus, le compilateur tentera d'utiliser l'expression réécrite (42 <=> a) < 0 , mais constatera qu'il n'y a pas de conversion d' IntWrapper en int pour satisfaire le côté gauche, de sorte que l'expression réécrite est ignorée. Le compilateur appelle également l'expression «synthétisée» 0 < (a <=> 42) et détecte qu'une conversion de int vers IntWrapper via son constructeur de conversion, donc ce candidat est utilisé.

Le but des expressions synthétisées est d'éviter la confusion d'écrire des modèles de fonction friend pour combler les lacunes dans lesquelles votre objet peut être converti à partir d'autres types. Les expressions synthétisées sont généralisées à 0 @ (b <=> a) .

Types plus complexes


L'opérateur de vaisseau spatial généré par le compilateur ne s'arrête pas aux membres individuels des classes; il génère l'ensemble de comparaisons correct pour tous les sous-objets de vos types:

 struct Basics {  int i;  char c;  float f;  double d;  auto operator<=>(const Basics&) const = default; }; struct Arrays {  int ai[1];  char ac[2];  float af[3];  double ad[2][2];  auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays {  auto operator<=>(const Bases&) const = default; }; int main() {  constexpr Bases a = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  constexpr Bases b = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  static_assert(a == b);  static_assert(!(a != b));  static_assert(!(a < b));  static_assert(a <= b);  static_assert(!(a > b));  static_assert(a >= b); } 

Le compilateur sait comment développer les membres de classe qui sont des tableaux dans leurs listes de sous-objets et les comparer récursivement. Bien sûr, si vous voulez écrire vous-même le corps de ces fonctions, vous bénéficierez toujours de la réécriture des expressions par le compilateur.

Ressemble à un canard, nage comme un canard et charlatans comme operator==


Certaines personnes très intelligentes du comité de normalisation ont remarqué que l'opérateur du vaisseau spatial effectuera toujours une comparaison lexicographique des éléments, quoi qu'il arrive. L'exécution inconditionnelle de comparaisons lexicographiques peut conduire à un code inefficace, en particulier, avec l'opérateur d'égalité.

Un exemple canonique comparant deux lignes. Si vous avez la chaîne "foobar" et que vous la comparez avec la chaîne "foo" utilisant ==, vous pouvez vous attendre à ce que cette opération soit presque constante. Un algorithme de comparaison de chaînes efficace est le suivant:

  • Comparez d'abord la taille des deux lignes. Si les tailles sont différentes, retournez false
  • Sinon, parcourez pas à pas chaque élément de deux lignes et comparez-les jusqu'à ce qu'il y ait une différence ou que tous les éléments se terminent. Renvoie le résultat.

Conformément aux règles de l'opérateur du vaisseau spatial, nous devons commencer par comparer chaque élément jusqu'à ce que nous en trouvions un qui soit différent. Dans notre exemple, "foobar" et "foo" uniquement lorsque vous comparez 'b' et '\0' , vous retournez finalement false .

Pour lutter contre cela, il y avait l'article P1185R2 , qui détaille comment le compilateur réécrit et génère l' operator== indépendamment de l'opérateur du vaisseau spatial. Notre IntWrapper peut s'écrire comme suit:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator==(const IntWrapper&) const = default; }; 

Un pas de plus ... cependant, il y a de bonnes nouvelles; vous n'avez pas vraiment besoin d'écrire le code ci-dessus, car il suffit d'écrire auto operator<=>(const IntWrapper&) const = default pour que le compilateur génère implicitement un operator== séparé et plus efficace operator== pour vous!

Le compilateur applique une règle de «réécriture» légèrement modifiée, spécifique à == et != , Où dans ces opérateurs ils sont réécrits en termes d' operator== plutôt que d' operator<=> . Cela signifie que != Bénéficie également de l'optimisation.

L'ancien code ne cassera pas


À ce stade, vous pourriez penser: eh bien, si le compilateur est autorisé à effectuer cette opération de réécriture d'opérateur, que se passera-t-il si j'essaie de déjouer le compilateur:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } 

La réponse n'est pas grave. Le modèle de résolution de surcharge en C ++ est l'arène dans laquelle tous les candidats s'affrontent. Dans cette bataille particulière, nous en avons trois:

  • IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
  • IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)

(réécrit)

  • IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)

(synthétisé)

Si nous adoptions des règles de résolution de surcharge en C ++ 17, le résultat de cet appel serait mélangé, mais les règles de résolution de surcharge C ++ 20 ont été modifiées afin que le compilateur puisse résoudre cette situation à la surcharge la plus logique.

Il y a une phase de résolution de surcharge lorsque le compilateur doit effectuer une série de passes supplémentaires. C ++ 20 a introduit un nouveau mécanisme dans lequel les surcharges qui ne sont pas écrasées ou synthétisées sont préférées, ce qui fait de notre IntWrapper::operator< surcharge le meilleur candidat et résout l'ambiguïté. Le même mécanisme empêche l'utilisation de candidats synthétisés au lieu des expressions réécrites habituelles.

Réflexions finales


L'opérateur de vaisseau spatial est un ajout bienvenu au C ++, car il peut vous aider à simplifier votre code et à écrire moins, et parfois moins c'est mieux. Alors attachez et contrôlez votre vaisseau spatial C ++ 20!

Nous vous invitons à sortir et à essayer l'opérateur de vaisseau spatial, il est disponible dès maintenant dans Visual Studio 2019 sous /std:c++latest ! Remarque: les modifications apportées au P1185R2 seront disponibles dans Visual Studio 2019 version 16.2. Veuillez garder à l'esprit que l'opérateur du vaisseau spatial fait partie de C ++ 20 et est soumis à certaines modifications jusqu'au moment où C ++ 20 est finalisé.

Comme toujours, nous attendons vos commentaires. N'hésitez pas à envoyer vos commentaires par e-mail à visualcpp@microsoft.com , via Twitter @visualc ou Facebook Microsoft Visual Cpp .

Si vous rencontrez d'autres problèmes avec MSVC dans VS 2019, veuillez nous en informer via l'option «Signaler un problème» , soit à partir du programme d'installation, soit à partir de l'IDE Visual Studio lui-même. Pour des suggestions ou des rapports de bogues, écrivez-nous via DevComm.

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


All Articles