Operaciones de comparación en C ++ 20

La reunión en Colonia ha pasado, el estándar C ++ 20 se ha reducido a un aspecto más o menos terminado (al menos hasta la aparición de notas especiales), y me gustaría hablar sobre una de las próximas innovaciones. Este es un mecanismo que generalmente se llama operador <=> (el estándar lo define como un "operador de comparación de tres vías", pero tiene el apodo informal "nave espacial"), pero creo que su alcance es mucho más amplio.

No solo tendremos un nuevo operador: la semántica de las comparaciones sufrirá cambios significativos a nivel del lenguaje mismo.

Incluso si no puede obtener nada más de este artículo, recuerde esta tabla:
La igualdad
Racionalización
Básico
==
<=>
Derivados
! =
< , > , <= , > =

Ahora tendremos un nuevo operador, <=> , pero, lo que es más importante, los operadores ahora están sistematizados. Hay operadores básicos y operadores derivados: cada grupo tiene sus propias capacidades.

Hablaremos de estas características brevemente en la introducción y consideraremos con más detalle en las siguientes secciones.

Los operadores básicos pueden invertirse (es decir, reescribirse con el orden inverso de los parámetros). Las declaraciones derivadas pueden reescribirse a través de la declaración base correspondiente. Ni los candidatos convertidos ni reescritos generan nuevas funciones, son simplemente reemplazos a nivel de código fuente y se seleccionan de un conjunto extendido de candidatos . Por ejemplo, la expresión a <9 ahora puede evaluarse como a.operator <=> (9) <0 , y la expresión 10! = B as ! Operator == (b, 10) . Esto significa que será posible prescindir de uno o dos operadores donde, para lograr el mismo comportamiento, ahora se requiere escribir manualmente 2, 4, 6 o incluso 12 operadores. A continuación se presentará una breve descripción de las reglas junto con una tabla de todas las posibles transformaciones.

Los operadores básicos y derivados se pueden definir como predeterminados . En el caso de operadores básicos, esto significa que el operador se aplicará a cada miembro en el orden de declaración; en el caso de operadores derivados, se utilizarán los candidatos reescritos.

Cabe señalar que no existe tal transformación en la que un operador de un tipo (es decir, igualdad u ordenamiento) pueda expresarse en términos de un operador de otro tipo. En otras palabras, las columnas de nuestra tabla no dependen en modo alguno entre sí. La expresión a == b nunca será evaluada como operador <=> (a, b) == 0 implícitamente (pero, por supuesto, nada le impide definir su operador == usando el operador <=> si lo desea).

Considere un pequeño ejemplo en el que mostramos cómo se ve el código antes y después de aplicar la nueva funcionalidad. Escribiremos un tipo de cadena que no distinga entre mayúsculas y minúsculas, CIString , cuyos objetos se pueden comparar entre sí y con char const * .

En C ++ 17, para nuestra tarea, necesitamos escribir 18 funciones de comparación:

class CIString { string s; public: friend bool operator==(const CIString& a, const CIString& b) { return assize() == bssize() && ci_compare(asc_str(), bsc_str()) == 0; } friend bool operator< (const CIString& a, const CIString& b) { return ci_compare(asc_str(), bsc_str()) < 0; } friend bool operator!=(const CIString& a, const CIString& b) { return !(a == b); } friend bool operator> (const CIString& a, const CIString& b) { return b < a; } friend bool operator>=(const CIString& a, const CIString& b) { return !(a < b); } friend bool operator<=(const CIString& a, const CIString& b) { return !(b < a); } friend bool operator==(const CIString& a, const char* b) { return ci_compare(asc_str(), b) == 0; } friend bool operator< (const CIString& a, const char* b) { return ci_compare(asc_str(), b) < 0; } friend bool operator!=(const CIString& a, const char* b) { return !(a == b); } friend bool operator> (const CIString& a, const char* b) { return b < a; } friend bool operator>=(const CIString& a, const char* b) { return !(a < b); } friend bool operator<=(const CIString& a, const char* b) { return !(b < a); } friend bool operator==(const char* a, const CIString& b) { return ci_compare(a, bsc_str()) == 0; } friend bool operator< (const char* a, const CIString& b) { return ci_compare(a, bsc_str()) < 0; } friend bool operator!=(const char* a, const CIString& b) { return !(a == b); } friend bool operator> (const char* a, const CIString& b) { return b < a; } friend bool operator>=(const char* a, const CIString& b) { return !(a < b); } friend bool operator<=(const char* a, const CIString& b) { return !(b < a); } }; 

En C ++ 20, puede hacer solo 4 funciones:

 class CIString { string s; public: bool operator==(const CIString& b) const { return s.size() == bssize() && ci_compare(s.c_str(), bsc_str()) == 0; } std::weak_ordering operator<=>(const CIString& b) const { return ci_compare(s.c_str(), bsc_str()) <=> 0; } bool operator==(char const* b) const { return ci_compare(s.c_str(), b) == 0; } std::weak_ordering operator<=>(const char* b) const { return ci_compare(s.c_str(), b) <=> 0; } }; 

Te diré lo que significa todo, con más detalle, pero primero, regresemos un poco y recordemos cómo las comparaciones funcionaron con el estándar C ++ 20.

Comparaciones en estándares de C ++ 98 a C ++ 17


Las operaciones de comparación no han cambiado mucho desde la creación del lenguaje. Teníamos seis operadores: == ,! = , < , > , <= Y > = . El estándar define cada uno de ellos para los tipos incorporados, pero en general obedecen las mismas reglas. Al evaluar cualquier expresión a @ b (donde @ es uno de los seis operadores de comparación), el compilador busca funciones miembro, funciones libres y candidatos incorporados llamados operator @ , que se pueden llamar con el tipo A o B en el orden especificado. El candidato más adecuado se selecciona de ellos. Eso es todo De hecho, todos los operadores trabajaron de la misma manera: la operación < no difirió de << .

Un conjunto de reglas tan simple es fácil de aprender. Todos los operadores son absolutamente independientes y equivalentes. No importa lo que los humanos sepamos acerca de la relación fundamental entre == y ! = Operaciones. En términos de lenguaje, este es uno y el mismo. Usamos modismos. Por ejemplo, definimos el operador ! = Through == :

 bool operator==(A const&, A const&); bool operator!=(A const& lhs, A const& rhs) { return !(lhs == rhs); } 

Del mismo modo, a través del operador < definimos todos los demás operadores de relación. Usamos estos modismos porque, a pesar de las reglas del lenguaje, realmente no consideramos que los seis operadores sean equivalentes. Aceptamos que dos de ellos son básicos ( == y < ), y a través de ellos todos los demás ya están expresados.

De hecho, la Biblioteca de plantillas estándar se basa completamente en estos dos operadores, y la gran cantidad de tipos en el código explotado contiene definiciones de solo uno de ellos o de ambos.

Sin embargo, el operador < no es muy adecuado para el rol base por dos razones.

Primero, no se puede garantizar que otros operadores de relaciones expresen a través de él. Sí, a> b significa exactamente lo mismo que b <a , pero no es cierto que a <= b signifique exactamente lo mismo que ! (B <a) . Las dos últimas expresiones serán equivalentes si hay una propiedad de tricotomía en la que para cualquiera de los dos valores solo una de las tres afirmaciones es verdadera: a <b , a == bo a a> b . En presencia de una tricotomía, la expresión a <= b significa que estamos tratando con el primer o el segundo caso ... y esto es equivalente a la afirmación de que no estamos tratando con el tercer caso. Por lo tanto (a <= b) ==! (A> b) ==! (B <a) .

Pero, ¿qué pasa si la actitud no posee la propiedad de la tricotomía? Esto es característico de las relaciones de orden parcial. Un ejemplo clásico son los números de coma flotante para los cuales cualquiera de las operaciones 1.f <NaN , 1.f == NaN y 1.f> NaN da falso . Por lo tanto, 1.f <= NaN también da una mentira , pero al mismo tiempo ! (NaN <1.f) es cierto .

La única forma de implementar el operador <= en términos generales a través de los operadores básicos es pintar ambas operaciones como (a == b) || (a <b) , que es un gran paso hacia atrás si todavía tenemos que lidiar con el orden lineal, ya que no se llamará a una función, sino a dos (por ejemplo, la expresión “abc..xyz9” <= “abc ..xyz1 " deberá reescribirse como (" abc..xyz9 "==" abc..xyz1 ") || (" abc..xyz9 "<" abc..xyz1 ") y dos veces para comparar la línea completa).

En segundo lugar, el operador < no es muy adecuado para el papel básico debido a las peculiaridades de su uso en las comparaciones lexicográficas. Los programadores a menudo cometen este error:

 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } }; 

Para definir el operador == para una colección de elementos, es suficiente aplicar == a cada miembro una vez, pero esto no funcionará con el operador < . Desde el punto de vista de esta implementación, los conjuntos A {1, 2} y A {2, 1} se considerarán equivalentes (ya que ninguno de ellos es menor que el otro). Para solucionar esto, aplique el operador < dos veces a cada miembro, excepto el último:

 bool operator< (A const& rhs) const { if (t < rhs.t) return true; if (rhs.t < t) return false; return u < rhs.u; } 

Finalmente, para garantizar el correcto funcionamiento de las comparaciones de objetos heterogéneos, es decir Para asegurarse de que las expresiones a == 10 y 10 == a significan lo mismo, generalmente recomiendan escribir comparaciones como funciones libres. De hecho, esta es generalmente la única forma de implementar tales comparaciones. Esto es inconveniente porque, en primer lugar, debe supervisar el cumplimiento de esta recomendación y, en segundo lugar, generalmente tiene que declarar tales funciones como amigos ocultos para una implementación más conveniente (es decir, dentro del cuerpo de la clase).

Tenga en cuenta que al comparar objetos de diferentes tipos no siempre es necesario escribir operator == (X, int) ; También pueden significar casos en los que int se puede convertir a X implícitamente.

Resumamos las reglas al estándar C ++ 20:

  • Todas las declaraciones se manejan de la misma manera.
  • Utilizamos modismos para facilitar la implementación. Los operadores == y < tomamos los modismos básicos y expresamos los operadores de relación restantes a través de ellos.
  • Eso es solo que el operador < no es muy adecuado para el papel de la base.
  • Es importante (y recomendado) escribir comparaciones de objetos heterogéneos como funciones libres.

Nuevo operador de pedido básico: <=>


El cambio más significativo y notable en el trabajo de comparaciones en C ++ 20 es la adición de un nuevo operador: operador <=> , un operador de comparación de tres vías.

Ya estamos familiarizados con las comparaciones de tres vías por las funciones memcmp / strcmp en C y basic_string :: compare () en C ++. Todos devuelven un valor de tipo int , que está representado por un número positivo arbitrario si el primer argumento es mayor que el segundo, 0 si son iguales y un número negativo arbitrario en caso contrario.

El operador de "nave espacial" no devuelve un valor int , sino un objeto que pertenece a una de las categorías de comparación, cuyo valor refleja el tipo de relación entre los objetos comparados. Hay tres categorías principales:

  • strong_ordering : una relación de orden lineal en la que la igualdad implica la intercambiabilidad de elementos (es decir, (a <=> b) == strong_ordering :: equal implica que f (a) == f (b) se cumple para todas las funciones adecuadas f El término "función adecuada" intencionalmente no tiene una definición clara, pero no incluye funciones que devuelvan las direcciones de sus argumentos o la capacidad () del vector, etc. Solo nos interesan las propiedades "esenciales", que también son muy vagas, pero pueden ser condicionalmente supongamos que estamos hablando del valor del tipo. El valor del vector está contenido en él m elementos, pero no su dirección, etc.). Esta categoría incluye los siguientes valores: strong_ordering :: mayor , strong_ordering :: igual y strong_ordering :: less .
  • débil_orden : una relación de orden lineal en la que la igualdad define solo una cierta clase de equivalencia. Un ejemplo clásico es la comparación de cadenas sin distinción entre mayúsculas y minúsculas, cuando dos objetos pueden ser de orden débil :: equivalente , pero no son estrictamente iguales (esto explica el reemplazo de la palabra igual por equivalente en el nombre del valor).
  • parcial_orden : relación de orden parcial. En esta categoría, se agrega un valor más a los valores mayor , equivalente y menor (como en débil_ordenamiento ): no ordenado ("desordenado"). Se puede usar para expresar relaciones de orden parcial en un sistema de tipos: 1.f <=> NaN da el valor partial_ordering :: unordered .

Trabajará principalmente con la categoría strong_ordering ; Esta es también la categoría óptima para su uso por defecto. Por ejemplo, 2 <=> 4 devuelve strong_ordering :: less y 3 <=> -1 devuelve strong_ordering :: mayor .

Las categorías de un orden superior pueden reducirse implícitamente a categorías de un orden más débil (es decir, strong_ordering es reducible a débil_ordering ). En este caso, el tipo actual de relación se conserva (es decir, strong_ordering :: equal se convierte en débil_ordering :: equivalente ).

Los valores de las categorías de comparación se pueden comparar con el literal 0 (no con ningún int y no con int igual a 0 , sino simplemente con el literal 0 ) utilizando uno de los seis operadores de comparación:

 strong_ordering::less < 0 // true strong_ordering::less == 0 // false strong_ordering::less != 0 // true strong_ordering::greater >= 0 // true partial_ordering::less < 0 // true partial_ordering::greater > 0 // true // unordered -  ,   //       partial_ordering::unordered < 0 // false partial_ordering::unordered == 0 // false partial_ordering::unordered > 0 // false 

Es gracias a una comparación con el literal 0 que podemos implementar los operadores de relación: a @ b es equivalente a (a <=> b) @ 0 para cada uno de estos operadores.

Por ejemplo, 2 <4 se puede calcular como (2 <=> 4) <0 , que se convierte en strong_ordering :: less <0 y da el valor verdadero .

El operador <=> se ajusta a la función del elemento base mucho mejor que el operador < , ya que elimina ambos problemas de este último.

Primero, se garantiza que la expresión a <= b será equivalente a (a <=> b) <= 0 incluso con ordenamiento parcial. Para dos valores desordenados, a <=> b dará el valor partial_ordered :: unordered , y partial_ordered :: unordered <= 0 dará false , que es lo que necesitamos. Esto es posible porque <=> puede devolver más variedades de valores: por ejemplo, la categoría de orden parcial contiene cuatro valores posibles. Un valor de tipo bool solo puede ser verdadero o falso , por lo que antes no podíamos distinguir entre comparaciones de valores ordenados y no ordenados.

Para mayor claridad, considere un ejemplo de una relación de orden parcial que no está relacionada con los números de coma flotante. Supongamos que queremos agregar un estado NaN a un tipo int , donde NaN es solo un valor que no forma un par ordenado con ningún valor involucrado. Puede hacer esto usando std :: opcional para almacenarlo:

 struct IntNan { std::optional<int> val = std::nullopt; bool operator==(IntNan const& rhs) const { if (!val || !rhs.val) { return false; } return *val == *rhs.val; } partial_ordering operator<=>(IntNan const& rhs) const { if (!val || !rhs.val) { //  unordered   //     return partial_ordering::unordered; } // <=>   strong_ordering  int, //        partial_ordering return *val <=> *rhs.val; } }; IntNan{2} <=> IntNan{4}; // partial_ordering::less IntNan{2} <=> IntNan{}; // partial_ordering::unordered //     .    IntNan{2} < IntNan{4}; // true IntNan{2} < IntNan{}; // false IntNan{2} == IntNan{}; // false IntNan{2} <= IntNan{}; // false 

El operador <= devuelve el valor correcto porque ahora podemos expresar más información a nivel del lenguaje mismo.

En segundo lugar, para obtener toda la información necesaria, basta con aplicar <=> una vez, lo que facilita la implementación de la comparación lexicográfica:
 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } strong_ordering operator<=>(A const& rhs) const { //    //  t.   != 0 (..  t // ),    //   if (auto c = t <=> rhs.t; c != 0) return c; //     //    return u <=> rhs.u; }; 

Vea P0515 , la oración original para agregar el operador <=>, para una discusión más detallada .

Nuevas características del operador


No solo tenemos a nuestra disposición un nuevo operador. Al final, si el ejemplo que se muestra arriba con la declaración de la estructura A solo dice que en lugar de x <y ahora tenemos que escribir (x <=> y) <0 cada vez, a nadie le gustaría.

El mecanismo para resolver las comparaciones en C ++ 20 difiere notablemente del enfoque anterior, pero este cambio está directamente relacionado con el nuevo concepto de dos operadores de comparación básicos: == y <=> . Si antes era un idioma (grabación a través de == y < ), que usamos, pero que el compilador no sabía, ahora comprenderá esta diferencia.

Una vez más, le daré una tabla que ya vio al principio del artículo:
La igualdad
Racionalización
Básico
==
<=>
Derivados
! =
< , > , <= , > =

Cada uno de los operadores básicos y derivados recibió una nueva habilidad, que diré algunas palabras más.

Inversión de operadores básicos.


Como ejemplo, tome un tipo que solo se pueda comparar con int :

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } }; 

Desde el punto de vista de las viejas reglas, no es sorprendente que la expresión a == 10 funcione y se evalúe como a.operator == (10) .

Pero, ¿qué pasa con 10 == a ? En C ++ 17, esta expresión se consideraría un claro error de sintaxis. No hay tal operador. Para que este código funcione, tendría que escribir un operador simétrico == , que primero tomaría el valor de int , y luego A ... y tendría que implementarlo como una función libre.

En C ++ 20, los operadores básicos pueden invertirse. Para 10 == a, el compilador encontrará el operador candidato == (A, int) (de hecho, esta es una función miembro, pero para mayor claridad, lo escribo aquí como una función libre), y luego, adicionalmente, una variante con el orden inverso de los parámetros, es decir. . operador == (int, A) . Este segundo candidato coincide con nuestra expresión (e idealmente), por lo que lo elegiremos. La expresión 10 == a en C ++ 20 se evalúa como a.operator == (10) . El compilador entiende que la igualdad es simétrica.

Ahora expandiremos nuestro tipo para que pueda compararse con int no solo a través del operador de igualdad, sino también a través del operador de pedido:

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

Nuevamente, la expresión a <=> 42 funciona bien y se calcula de acuerdo con las reglas anteriores como a.operator <=> (42) , pero 42 <=> a estaría mal desde el punto de vista de C ++ 17, incluso si el operador < => ya existía en el idioma. Pero en C ++ 20, el operador <=> , como el operador == , es simétrico: reconoce candidatos invertidos. Para 42 <=> a, se encontrará un operador de función miembro <=> (A, int) (nuevamente, lo escribo aquí como una función libre solo para mayor claridad), así como un operador candidato sintético <=> (int, A) . Esta versión inversa coincide exactamente con nuestra expresión: la seleccionamos.

Sin embargo, 42 <=> a NO se calcula como un operador <=> (42) . Eso estaría mal. Esta expresión se evalúa como 0 <=> a.operator <=> (42) . Intenta descubrir por qué esta entrada es correcta.

Es importante tener en cuenta que el compilador no crea ninguna función nueva. Al calcular 10 == a , el nuevo operador operador == (int, A) no apareció, y al calcular 42 <=> a , el operador <=> (int, A) no apareció. Solo dos expresiones se reescriben a través de candidatos invertidos. Repito: no se crean nuevas funciones.

También tenga en cuenta que un registro con el orden inverso de los parámetros está disponible solo para operadores básicos, pero para derivados no lo está. Eso es:

 struct B { bool operator!=(int) const; }; b != 42; // ok   C++17,   C++20 42 != b; //    C++17,   C++20 

Reescritura de operadores derivados


Volvamos a nuestro ejemplo con la estructura A :

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

Tome la expresión a! = 17 . En C ++ 17, este es un error de sintaxis porque el operador! = El operador no existe. Sin embargo, en C ++ 20, para expresiones que contienen operadores de comparación de derivadas, el compilador también buscará los operadores básicos correspondientes y expresará comparaciones de derivadas a través de ellos.

Sabemos que en matemáticas, la operación ! = Esencialmente significa NO == . Ahora esto es conocido por el compilador. Para la expresión a! = 17, buscará no solo el operador! = Operadores , sino también el operador == (y, como en los ejemplos anteriores, el operador invertido == ). Para este ejemplo, encontramos un operador de igualdad que casi nos conviene: solo tenemos que reescribirlo de acuerdo con la semántica deseada: a! = 17 se calculará como ! (A == 17) .

Del mismo modo, 17! = A se calcula como ! A.operator == (17) , que es una versión reescrita y una versión invertida.

También se llevan a cabo transformaciones similares para los operadores de pedidos. Si escribiéramos un <9 , intentaríamos (sin éxito) encontrar el operador < , y también consideraríamos los candidatos básicos: operador <=> . El reemplazo correspondiente para los operadores de relación se ve así: a @ b (donde @ es uno de los operadores de relación) se calcula como (a <=> b) @ 0 . En nuestro caso, a.operator <=> (9) <0 . Del mismo modo, 9 <= a se calcula como 0 <= a.operator <=> (9) .

Tenga en cuenta que, como en el caso de la llamada, el compilador no crea ninguna función nueva para los candidatos reescritos. Simplemente se calculan de manera diferente, y todas las transformaciones se llevan a cabo solo en el nivel del código fuente.

Lo anterior me lleva al siguiente consejo:

SOLO OPERADORES BÁSICOS : defina solo operadores básicos (== y <=>) en su tipo.

Dado que los operadores básicos dan el conjunto completo de comparaciones, es suficiente definirlos solo. Esto significa que solo necesita 2 operadores para comparar objetos del mismo tipo (en lugar de 6, a partir de ahora) y solo 2 operadores para comparar diferentes tipos de objetos (en lugar de 12). Si solo necesita la operación de igualdad, simplemente escriba 1 función para comparar objetos del mismo tipo (en lugar de 2) y 1 función para comparar diferentes tipos de objetos (en lugar de 4). La clase std :: sub_match es un caso extremo: en C ++ 17 usa 42 operadores de comparación, y en C ++ 20 usa solo 8, mientras que la funcionalidad no se ve afectada de ninguna manera.

Dado que el compilador también considera candidatos invertidos, todos estos operadores pueden implementarse como funciones miembro. Ya no tiene que escribir funciones gratuitas solo por comparar objetos de diferentes tipos.

Reglas especiales para encontrar candidatos


Como ya mencioné, la búsqueda de candidatos para a @ b en C ++ 17 se llevó a cabo de acuerdo con el siguiente principio: encontramos todos los operadores operator @ y seleccionamos el más adecuado de ellos.

C ++ 20 utiliza un conjunto extendido de candidatos. Ahora buscaremos todos los operadores @ . Deje @@ ser el operador base para @ (puede ser el mismo operador). También encontramos todos los operadores @@ y para cada uno de ellos agregamos su versión invertida. De todos estos candidatos encontrados, seleccionamos el más adecuado.

Tenga en cuenta que la sobrecarga del operador está permitida en una sola pasada. No estamos tratando de sustituir a diferentes candidatos. Primero los recogemos todos, y luego elegimos el mejor de ellos. Si esto no existe, la búsqueda, como antes, falla.

Ahora tenemos muchos más candidatos potenciales y, por lo tanto, más incertidumbre. Considere el siguiente ejemplo:

 struct C { bool operator==(C const&) const; bool operator!=(C const&) const; }; bool check(C x, C y) { return x != y; } 

En C ++ 17, solo teníamos un candidato para x! = Y , y ahora hay tres: x.operator! = (Y) ,! X.operator == (y) y ! Y.operator == (x) . Que elegir ¡Son todos iguales! (Nota: el candidato y.operator! = (X) no existe, ya que solo se pueden invertir los operadores básicos).

Se han introducido dos reglas adicionales para eliminar esta incertidumbre. Los candidatos no convertidos son preferibles a los conversos; . , x.operator!=(y) «» !x.operator==(y) , «» !y.operator==(x) . , «» .

: operator@@ . . , .

-. — (, x < y , — (x <=> y) < 0 ), (, x <=> y void - , DSL), . . , bool ( : operator== bool , ?)

Por ejemplo:

 struct Base { friend bool operator<(const Base&, const Base&); // #1 friend bool operator==(const Base&, const Base&); }; struct Derived : Base { friend void operator<=>(const Derived&, const Derived&); // #2 }; bool f(Derived d1, Derived d2) { return d1 < d2; } 

d1 < d2 : #1 #2 . — #2 , , , . , d1 < d2 (d1 <=> d2) < 0 . , void 0 — , . , - , #1 .


, , C++17, . , - . :

  • ( )
  • ,
  • , .

, . .

. , , , , , ( ). , :

1
2
a == b
b == a

a != b
!(a == b)
!(b == a)
a <=> b
0 <=> (b <=> a)

a < b
(a <=> b) < 0
(b <=> a) > 0
a <= b
(a <=> b) <= 0
(b <=> a) >= 0
a > b
(a <=> b) > 0
(b <=> a) < 0
a >= b
(a <=> b) >= 0
(b <=> a) <= 0

« » , , .. a < b 0 < (b <=> a) , , , .


C++17 . . :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } bool operator!=(A const& rhs) const { return !(*this == rhs); } bool operator< (A const& rhs) const { //    ,     , //     ?:  &&/|| if (t < rhs.t) return true; if (rhs.t < t) return false; if (u < rhs.u) return true; if (rhs.u < u) return false; return v < rhs.v; } bool operator> (A const& rhs) const { return rhs < *this; } bool operator<=(A const& rhs) const { return !(rhs < *this); } bool operator>=(A const& rhs) const { return !(*this < rhs); } }; 

- std::tie() , .

, : :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } strong_ordering operator<=>(A const& rhs) const { //   T if (auto c = t <=> rhs.t; c != 0) return c; // ...  U if (auto c = u <=> rhs.u; c != 0) return c; // ...  V return v <=> rhs.v; } }; 

. <=> < . , . c != 0 , , ( ), .

. C++20 , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; strong_ordering operator<=>(A const& rhs) const = default; }; 

, . , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; auto operator<=>(A const& rhs) const = default; }; 

. , , :

 struct A { T t; U u; V v; auto operator<=>(A const& rhs) const = default; }; 

, , . : operator== , operator<=> .


C++20: . . , , , .


PVS-Studio , <=> . , -. , , (. " "). ++ .

PVS-Studio <, :

 bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } 

. , - . .

: Comparisons in C++20 .

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


All Articles