C++ : l’opérateur spaceship

Rédigé par Nicolas K Aucun commentaire

Une fonctionnalité dont on a besoin régulièrement et dans beaucoup de situations, c’est la comparaison. Comparer deux objets est en effet utile pour pouvoir trier des objets, filtrer des données, ou encore simplement comme condition à l’exécution d’un bloc de code.

En C++, il fallait jusqu’à maintenant implémenter les différents opérateurs <,>,<=,>=,==,!= selon ce dont on avait besoin, et leurs implémentations étaient souvent répétitives et verbeuses. Par ailleurs, il fallait surcharger ces opérateurs pour les différents types de couples de données. Enfin, dernier problème : d’un point de vue fonctionnel, toutes les comparaisons ne se valent pas : on peut clairement comparer deux variables de type int, mais comparer deux variables de type float n’a parfois pas de sens (notamment si l’une des deux vaut NaN).

Pour régler tous ces problèmes, un nouvel opérateur va faire son apparition en C++20 : l’opérateur "spaceship" dont cet article va détailler le fonctionnement.

Fonctionnement

Commençons par les présentations : voici l’opérateur spaceship : <=>, également appelé "comparateur trilatéral" ou en anglais "Three-way comparison operator".

Son fonctionnement est le suivant :

(a <=> b) < 0 vaut true si a < b

(a <=> b) == 0 vaut true si a == b

(a <=> b) > 0 vaut true si a > b

À première vue, on peut se dire "ok, mais quel est l’avantage de ce truc ?!". Et c’est vrai que sans quelques autres "détails" que je vais présenter, l’intérêt pourrait sembler très limité. Mais voici pourquoi c’est une énorme avancée :

Une surcharge pour les dominer toutes

Vous avez besoin dans votre programme des opérateurs <,>,<=,>=,== et != sur un type personnel. Sans l’opérateur spaceship, il faut toutes les surcharger.

Avec l’opérateur spaceship, vous pouvez implémenter seulement <=> et le compilateur en déduira automatiquement tous les opérateurs de comparaison. Plus exactement, il le fera si ces dits opérateurs ne sont pas surchargés. Mais mieux que ça : on gagnera ainsi la symétrie : puisque (a <=> b) < 0 est équivalent à 0 < (b <=> a), il n’est pas nécessaire d’implémenter l’opérateur pour les deux ordres possibles d’arguments.

L’implémentation par défaut

Deuxième avantage non négligeable : on peut demander au compilateur d’implémenter l’opérateur pour nous grâce au mot-clé default :

auto operator<=>(x const&) = default;

En écrivant ceci, le compilateur construira une comparaison à partir des classes mères et des attributs (dans l’ordre de déclaration).

Et pour les types qui n’ont pas cet opérateur de défini ?

Pas de panique : si l’opérateur spaceship n’est pas défini mais que les opérateurs de comparaison sont quant à eux bien définis, vous pouvez utiliser la fonction std::compare_3way qui va appeler l’opérateur spaceship s’il existe ou utiliser les opérateurs de comparaison binaires sinon.

Cette fonction permet notamment d’écrire du code générique (templates) qui fonctionne quel que soit le type, que l’opérateur spaceship soit défini ou non.

Toutes les comparaisons ne se valent pas ?

Dernière chose, et sans doute la plus importante : l’opérateur spaceship gère la "force" des comparaisons.

Petit exercice :

if(a < b || a==b || a > b) {
    std::cout << "test";
}

Vous pensez que le code précédent affiche toujours "test" ? En êtes-vous certain ? Non : si a et b sont des float et que a ou b vaut NaN, "test" ne sera pas affiché. Perturbant n’est-ce pas ?

Comme je le disais plus haut, deux variables de type int peuvent toujours être comparées, on parle d’ordre fort. Au contraire, deux valeurs de type float ne le peuvent pas toujours (si l’une vaut NaN notamment). Dans ce cas, on parle d’ordre partiel car toutes les valeurs ne peuvent pas être comparées (et donc ordonnées).

De la même façon, il peut arriver que toutes les valeurs puissent être comparées mais que le fait que a>b soit faux et que a<b soit également faux n’implique pas que a==b. C’est le cas si on décide par exemple de comparer deux objets de type Rectangle et qu’on compare leurs aires. Deux rectangles peuvent avoir la même aire et ne pas être pour autant strictement égaux. On peut avoir besoin en effet parfois de faire la distinction entre une "équivalence" (aire identique dans notre exemple) et une égalité stricte.

Dans l’exemple précédent : les aires des rectangles sont les mêmes (équivalents à 6 carrés) mais ils sont tous différents. On parle ici d’ordre faible.

Alors comment faire la distinction entre tous ces cas avec un seul opérateur ?

Avec le type de retour. Pour chacun de ces cas, il existe un type dédié : std::strong_ordering pour l’ordre fort, std::partial_ordering pour l’ordre faible et std::weak_ordering pour l’ordre faible. Et lorsqu’on recherche l’égalité, ces types peuvent être convertis en std::weak_equality ou std::strong_equality, ces conversions se faisant comme suit :

Toutes ces distinctions permettent notamment d’écrire du code générique (templates) et de gérer tous les cas.

Conclusion

Cet opérateur est vraiment prometteur : sa seule implémentation remplace l’implémentation de tout le panel d’opérateurs de comparaison, il offre une comparaison par défaut, il gère la sémantique de la comparaison (ordre faible, partiel ou fort). Que demander de plus ? Seul inconvénient à mes yeux : il ne sera disponible qu’à partir de C++20.

Sources :

Classé dans : Tutos Mots clés : aucun

Les commentaires sont fermés.

Fil RSS des commentaires de cet article