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
où
@
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.