<=>
. Il y a un moment, notre propre marque Simon a publié un article détaillant certaines informations concernant ce nouvel opérateur ainsi que des informations conceptuelles sur ce qu'il est et ce qu'il fait. Le but de cet article est d'explorer quelques applications concrètes de cet étrange nouvel opérateur et de son homologue associé, l' operator==
(oui, il a été changé, pour le mieux!), Tout en fournissant quelques lignes directrices pour son utilisation dans le code de tous les jours.
Comparaisons
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 aux yeux d'aigle remarqueront que c'est en fait encore moins verbeux qu'il ne devrait l'être dans le code pré-C ++ 20 car ces fonctions devraient en fait toutes être des amis non membres, plus à ce sujet plus tard.
C'est beaucoup de code passe-partout à écrire juste pour m'assurer que mon type est comparable à quelque chose du même type. Eh bien, OK, nous nous en occupons pendant un certain temps. Vient ensuite quelqu'un qui écrit 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 ce programme ne se compilera pas.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
Ah! Le problème est que nous avons oublié
constexpr
sur notre fonction de comparaison, drat! Donc, on va et ajoute constexpr
à tous les opérateurs de comparaison. Quelques jours plus tard, quelqu'un va ajouter un assistant is_gt
mais remarque que tous les opérateurs de comparaison n'ont pas de spécification d'exception et passent par le même processus fastidieux d'ajout de noexcept
à chacune des 5 surcharges.C'est là que le nouvel opérateur de vaisseau spatial de C ++ 20 intervient pour nous aider. Voyons comment l'
IntWrapper
originale peut être écrite dans un 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égorie de comparaison nécessaires pour que l'opérateur du vaisseau spatial retourne un type approprié pour notre fonction par défaut. Dans l'extrait ci-dessus, le type de retour auto
sera déduit de std::strong_ordering
.Non seulement nous avons supprimé 5 lignes superflues, mais nous n'avons même pas besoin de définir quoi que ce soit, le compilateur le fait pour nous! Notre
is_lt
reste inchangé et fonctionne juste tout en étant constexpr
même si nous n'avons pas explicitement spécifié cela dans notre operator<=>
défaut operator<=>
. C'est bien beau, mais certaines personnes peuvent se gratter la tête pour savoir pourquoi is_lt
est toujours autorisé à compiler même s'il n'utilise même pas du tout l'opérateur du vaisseau spatial. Explorons la réponse à cette question.Réécrire des expressions
En C ++ 20, le compilateur est introduit dans un nouveau concept appelé expressions «réécrites». L'opérateur du vaisseau spatial, avec l'
operator==
, fait partie des deux premiers candidats soumis à des expressions réécrites. Pour un exemple plus concret de réécriture d'expression, décomposons l'exemple fourni dans is_lt
.Pendant la résolution de surcharge, le compilateur va sélectionner parmi un ensemble de candidats viables, qui correspondent tous à l'opérateur que nous recherchons. Le processus de collecte des candidats est très légèrement modifié pour le cas des opérations relationnelles et d'équivalence où le compilateur doit également rassembler des candidats spéciaux réécrits et synthétisés ( [over.match.oper] /3.4 ).
Pour notre expression
a < b
la norme stipule que nous pouvons rechercher le type de a pour un operator<=>
ou un operator<=>
fonction de portée d'espace de noms operator<=>
qui accepte son type. Donc, le compilateur le fait et il constate qu'en fait, le type de 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 de surcharge normale.Vous pouvez vous demander pourquoi cette expression réécrite est valide et correcte. La justesse de l'expression découle en fait de la sémantique fournie par l'opérateur du vaisseau spatial. Le
<=>
est une comparaison à trois qui implique que vous obtenez non seulement un résultat binaire, mais un ordre (dans la plupart des cas) et si vous avez un ordre, vous pouvez exprimer cet ordre en termes d'opérations relationnelles. Un exemple rapide, l'expression 4 <=> 5 en C ++ 20 vous donnera le résultat std::strong_ordering::less
. Le résultat std::strong_ordering::less
implique que 4
n'est pas seulement différent de 5
mais qu'il est strictement inférieur à cette valeur, ce qui rend l'application de l'opération (4 <=> 5) < 0
correcte et exacte pour décrire notre résultat.En utilisant les informations ci-dessus, le compilateur peut prendre n'importe quel opérateur relationnel généralisé (c'est-à-dire
<
, >
, etc.) et le réécrire en termes d'opérateur de vaisseau spatial. Dans la norme, l'expression réécrite est souvent appelée (a <=> b) @ 0
où le @
représente toute opération relationnelle.Synthétiser des expressions
Les lecteurs ont peut-être remarqué la mention subtile des expressions "synthétisées" ci-dessus et ils jouent également un rôle dans ce processus de réécriture d'opérateur. Considérons une fonction de prédicat différente:
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 a du sens dans la version pré-C ++ 20, et la façon de résoudre ce problème serait d'ajouter quelques fonctions supplémentaires à
IntWrapper
qui prennent un côté gauche d' int
. Si vous essayez de créer cet exemple avec un compilateur C ++ 20 et notre définition C ++ 20 d' IntWrapper
vous remarquerez peut-être que, là encore, «ça marche» - un autre gratte-tête. Examinons pourquoi le code ci-dessus est toujours autorisé à compiler en C ++ 20.Pendant la résolution de surcharge, le compilateur rassemblera également ce que la norme appelle des candidats «synthétisés» ou une expression réécrite avec l'ordre des paramètres inversé. Dans l'exemple ci-dessus, le compilateur essaiera d'utiliser l'expression réécrite
(42 <=> a) < 0
mais il trouvera qu'il n'y a pas de conversion d' IntWrapper
en int
pour satisfaire le côté gauche afin que l'expression réécrite soit supprimée. Le compilateur évoque également l'expression «synthétisée» 0 < (a <=> 42)
et constate qu'il y a une conversion de int
en IntWrapper
via son constructeur de conversion, donc ce candidat est utilisé.Le but des expressions synthétisées est d'éviter le désordre d'avoir à écrire le passe-partout des fonctions
friend
pour combler les lacunes où votre objet pourrait ê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 uniques des classes, il générera un ensemble correct de comparaisons 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 des classes qui sont des tableaux dans leurs listes de sous-objets et les comparer récursivement. Bien sûr, si vous vouliez écrire vous-même le corps de ces fonctions, vous bénéficiez toujours des expressions de réécriture du compilateur pour vous.
Ressemble à un canard, nage comme un canard et quacks 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. Des comparaisons lexicographiques inconditionnelles peuvent conduire à un code généré inefficace avec l'opérateur d'égalité en particulier.
L'exemple canonique compare deux chaînes. Si vous avez la chaîne
"foobar"
et que vous la comparez à la chaîne "foo"
utilisant == on s'attendrait à ce que cette opération soit presque constante. L'algorithme de comparaison de chaînes efficace est donc:- Comparez d'abord la taille des deux chaînes, si les tailles diffèrent, retournez
false
, sinon - parcourez chaque élément des deux chaînes à l'unisson et comparez jusqu'à ce que l'on diffère ou que la fin soit atteinte, renvoyez le résultat.
Selon les règles de l'opérateur de vaisseau spatial, nous devons commencer par la comparaison approfondie de chaque élément jusqu'à ce que nous trouvions celui qui est différent. Dans notre exemple de
"foobar"
et "foo"
uniquement en comparant 'b'
à '\0'
, vous retournez finalement false
.Pour lutter contre cela, il y avait un document, P1185R2, qui détaille un moyen pour le compilateur de réécrire et de générer l'
operator==
indépendamment de l'opérateur du vaisseau spatial. Notre IntWrapper
pourrait 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; };
Encore une étape ... cependant, il y a de bonnes nouvelles; vous n'avez pas réellement 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 l' operator==
séparé operator==
et plus efficace operator==
pour vous!Le compilateur applique une règle de «réécriture» légèrement modifiée spécifique à
==
et !=
Où ces opérateurs sont réécrits en termes d' operator==
et non d' operator<=>
. Cela signifie que !=
Bénéficie également de l'optimisation.L'ancien code ne se cassera pas
À ce stade, vous pourriez penser, OK si le compilateur est autorisé à effectuer cette opération de réécriture d'opérateur ce qui se passe lorsque 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 est que non. Le modèle de résolution de surcharge en C ++ a cette arène où tous les candidats se battent, et dans cette bataille spécifique, nous avons 3 candidats:
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 avions accepté les règles de résolution de surcharge en C ++ 17, le résultat de cet appel aurait été ambigu, mais les règles de résolution de surcharge C ++ 20 ont été modifiées pour permettre au compilateur de résoudre cette situation à la surcharge la plus logique.
Il y a une phase de résolution de surcharge où le compilateur doit effectuer une série de bris d'égalité. En C ++ 20, il y a un nouveau bris d'égalité qui stipule que nous devons préférer les surcharges qui ne sont pas réécrites ou synthétisées, ce qui fait de notre surcharge
IntWrapper::operator<
le meilleur candidat et résout l'ambiguïté. Ce même mécanisme empêche les candidats synthétisés de piétiner des expressions réécrites régulières.Pensées de clôture
L'opérateur de vaisseau spatial est un ajout bienvenu au C ++ et c'est l'une des fonctionnalités qui simplifiera et vous aidera à écrire moins de code, et, parfois, moins c'est plus. Alors, attachez-vous à l'opérateur de vaisseau spatial de 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 introduites via 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'à ce que C ++ 20 soit finalisé.Comme toujours, nous apprécions vos commentaires. N'hésitez pas à envoyer vos commentaires par e-mail à visualcpp@microsoft.com , via Twitter @visualc ou Facebook à Microsoft Visual Cpp . N'hésitez pas non plus à me suivre sur Twitter @starfreakclone .
Si vous rencontrez d'autres problèmes avec MSVC dans VS 2019, veuillez nous en informer via l'option Signaler un problème , à partir du programme d'installation ou de l'IDE Visual Studio lui-même. Pour des suggestions ou des rapports de bogues, faites-le nous savoir via DevComm.