Histoire de la question
Avant C++20, les développeurs implémentaient manuellement six opérateurs de comparaison pour les types triables. Ce code répétitif introduisait souvent des incohérences logiques subtiles entre les relations d'égalité et d'ordre. L'opérateur de vaisseau a été introduit pour consolider ces opérateurs en une seule opération canonique.
Le problème
Bien que l'opérateur <=> réduise la syntaxe, le compilateur s'appuie sur son type de retour pour synthétiser des expressions inversées comme b < a à partir de a > b. Sans savoir si l'ordre est fort, faible ou partiel, le compilateur ne peut pas générer ces réécritures en toute sécurité.
La solution
Le type de retour doit être std::strong_ordering, std::weak_ordering ou std::partial_ordering (ou implicitement convertible). Cette catégorie standard permet au compilateur de générer des candidats inversés et des vérifications d'égalité implicites. Retourner auto ou des types personnalisés désactive cette synthèse, nécessitant des surcharges asymétriques manuelles.
struct Widget { int id; // Correct : permet la génération de candidats inversés std::strong_ordering operator<=>(const Widget&) const = default; };
Scénario et problème
Le développement d'un SpatialIndex pour une géométrie accélérée par GPU nécessitait une structure BoundingBox avec un ordre faible strict pour l'insertion dans std::set. Les boîtes devaient être comparées à des tableaux de coordonnées brutes pour des requêtes spatiales.
Solution 1 : Surcharge manuelle des opérateurs
La mise en œuvre de douze surcharges (six pour BoundingBox, six pour les tableaux de coordonnées) offrait un contrôle explicite. Cependant, la verbosité risquait d'entraîner des erreurs de copier-coller entre l'opérateur < et l'opérateur >, et maintenir la cohérence lors des refactorisations s'est avéré fastidieux.
Solution 2 : Vaisseau par défaut retournant std::weak_ordering
Cela générait tous les opérateurs relationnels automatiquement à partir d'une seule déclaration. Le type de retour explicite permettait au compilateur de gérer des comparaisons inversées avec des tableaux de coordonnées. L'implémentation garantissait la sécurité des exceptions et la cohérence mathématique sans code répétitif.
Solution 3 : Retour automatique
L'utilisation de auto operator<=>(const BoundingBox&) const = default empêchait la synthèse de candidats inversés. La comparaison d'un tableau brut à gauche avec une BoundingBox à droite échouait à se compiler. Cette asymétrie brisait l'interface de requête spatiale.
Décision et résultat
Nous avons choisi la Solution 2 avec std::weak_ordering car les boîtes englobantes ont une équivalence (les boîtes qui s'intersectent sont considérées égales) mais pas une égalité mathématique. Cela a permis une intégration transparente avec les algorithmes standard tout en prenant en charge des comparaisons de coordonnées hétérogènes.
Pourquoi le compilateur synthétise-t-il l'opérateur == à partir de l'opérateur <=>, et quand cela est-il suboptimal ?
Le compilateur génère l'opérateur == comme ((*this <=> other) == 0). Cela offre de la cohérence mais impose une comparaison complète élément par élément même lors de la vérification de l'égalité. En définissant explicitement l'opérateur ==, on permet une évaluation court-circuit, retournant false immédiatement dès le premier membre différent.
Comment la définition de l'opérateur <=> en tant que membre plutôt qu'en tant qu'ami caché brise-t-elle la symétrie ?
Un membre l'opérateur <=> ne permet que des conversions implicites sur l'operande de droite lors de la résolution des surcharges. Cette asymétrie empêche des expressions comme double == MyClass de se compiler même si MyClass peut être construit à partir de double. Utiliser un ami caché permet une Recherche Dépendante de l'Argument (ADL), permettant aux deux opérandes de se convertir implicitement.
Qu'est-ce qui distingue std::compare_three_way de la comparaison manuelle des pointeurs ?
std::compare_three_way fournit un ordre total pour les pointeurs qui est cohérent dans tout l'espace d'adresse, y compris std::nullptr_t. Les comparaisons manuelles de pointeurs utilisant des opérateurs relationnels invoquent un comportement indéfini lors de la comparaison d'objets non liés. Utiliser l'objet de fonction standard garantit des sémantiques portables et bien définies pour le tri des pointeurs.