<=>
. Hubo una publicación hace un tiempo por nuestra propia Simon Brand que detallaba información sobre este nuevo operador junto con información conceptual sobre lo que es y hace. El objetivo de esta publicación es explorar algunas aplicaciones concretas de este extraño operador nuevo y su contraparte asociada, el operator==
(¡sí, se ha cambiado, para mejor!), Mientras se proporcionan algunas pautas para su uso en el código diario.
Comparaciones
No es raro ver código como el siguiente:
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); } };
Nota: los lectores con ojos de águila notarán que esto es incluso menos detallado de lo que debería ser en el código anterior a C ++ 20 porque estas funciones deberían ser en realidad amigos no miembros, más sobre eso más adelante.
Ese es un montón de código repetitivo para escribir solo para asegurarme de que mi tipo sea comparable a algo del mismo tipo. Bueno, está bien, lo tratamos por un tiempo. Luego viene alguien que escribe esto:
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; } int main() { static_assert(is_lt(0, 1)); }
Lo primero que notará es que este programa no se compilará.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
Ah! El problema es que olvidamos
constexpr
en nuestra función de comparación, ¡drat! Entonces uno va y agrega constexpr
a todos los operadores de comparación. Unos días después, alguien va y agrega un ayudante is_gt
pero se da cuenta de que todos los operadores de comparación no tienen una especificación de excepción y pasa por el mismo proceso tedioso de agregar noexcept
a cada una de las 5 sobrecargas.Aquí es donde interviene el nuevo operador de la nave espacial C ++ 20 para ayudarnos. Veamos cómo se puede escribir el
IntWrapper
original en un mundo C ++ 20: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; };
La primera diferencia que puede notar es la nueva inclusión de
<compare>
. El encabezado <compare>
es responsable de llenar el compilador con todos los tipos de categoría de comparación necesarios para que el operador de la nave espacial devuelva un tipo apropiado para nuestra función predeterminada. En el fragmento anterior, el tipo de retorno auto
se deducirá a std::strong_ordering
.No solo eliminamos 5 líneas superfluas, sino que ni siquiera tenemos que definir nada, ¡el compilador lo hace por nosotros! Nuestro
is_lt
permanece sin cambios y simplemente funciona mientras sigue siendo constexpr
, aunque no lo especificamos explícitamente en nuestro operator<=>
predeterminado operator<=>
. Eso está muy bien, pero algunas personas pueden estar rascándose la cabeza sobre por qué is_lt
todavía puede compilar aunque ni siquiera use el operador de la nave espacial. Exploremos la respuesta a esta pregunta.Reescribiendo expresiones
En C ++ 20, el compilador presenta un nuevo concepto referido a expresiones "reescritas". El operador de la nave espacial, junto con el
operator==
, se encuentran entre los dos primeros candidatos sujetos a expresiones reescritas. Para un ejemplo más concreto de reescritura de expresiones, analicemos el ejemplo proporcionado en is_lt
.Durante la resolución de sobrecarga, el compilador seleccionará de un conjunto de candidatos viables, todos los cuales coinciden con el operador que estamos buscando. El proceso de recopilación de candidatos se modifica ligeramente para el caso de operaciones relacionales y de equivalencia en las que el compilador también debe reunir candidatos especiales reescritos y sintetizados ( [over.match.oper] /3.4 ).
Para nuestra expresión
a < b
el estándar establece que podemos buscar el tipo de a para un operator<=>
o un operator<=>
función de alcance de espacio de nombres operator<=>
que acepta su tipo. Entonces el compilador lo hace y descubre que, de hecho, el tipo de un sí IntWrapper::operator<=>
. El compilador puede usar ese operador y reescribir la expresión a < b
as (a <=> b) < 0
. Esa expresión reescrita se utiliza como candidato para la resolución de sobrecarga normal.Puede que te preguntes por qué esta expresión reescrita es válida y correcta. La exactitud de la expresión en realidad proviene de la semántica que proporciona el operador de la nave espacial. El
<=>
es una comparación de tres vías que implica que usted obtiene no solo un resultado binario, sino un ordenamiento (en la mayoría de los casos) y si tiene un ordenamiento puede expresar ese ordenamiento en términos de cualquier operación relacional. Un ejemplo rápido, la expresión 4 <=> 5 en C ++ 20 le devolverá el resultado std::strong_ordering::less
. El resultado std::strong_ordering::less
implica que 4
no solo es diferente de 5
sino que es estrictamente menor que ese valor, esto hace que la aplicación de la operación (4 <=> 5) < 0
correcta y exacta para describir nuestro resultado.El uso de la información anterior del compilador puede tomar cualquier operador relacional generalizado (es decir,
<
, >
, etc.) y reescribirlo en términos del operador de la nave espacial. En el estándar, la expresión reescrita a menudo se denomina (a <=> b) @ 0
donde @
representa cualquier operación relacional.Sintetizar expresiones
Los lectores pueden haber notado la mención sutil de las expresiones "sintetizadas" arriba y también juegan un papel en este proceso de reescritura del operador. Considere una función de predicado diferente:
constexpr bool is_gt_42(const IntWrapper& a) { return 42 < a; }
Si usamos nuestra definición original para
IntWrapper
este código no se compilará.error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)
Esto tiene sentido en la tierra anterior a C ++ 20, y la forma de resolver este problema sería agregar algunas funciones de
friend
adicionales a IntWrapper
que toman un lado izquierdo de int
. Si intenta compilar esa muestra con un compilador C ++ 20 y nuestra definición C ++ 20 de IntWrapper
, puede notar que, de nuevo, "simplemente funciona", otro rascador de cabeza. Examinemos por qué el código anterior aún se puede compilar en C ++ 20.Durante la resolución de sobrecarga, el compilador también reunirá lo que el estándar se refiere como candidatos "sintetizados", o una expresión reescrita con el orden de los parámetros invertidos. En el ejemplo anterior, el compilador intentará usar la expresión reescrita
(42 <=> a) < 0
pero encontrará que no hay conversión de IntWrapper
a int
para satisfacer el lado izquierdo de modo que la expresión reescrita se descarte. El compilador también evoca la expresión "sintetizada" 0 < (a <=> 42)
y descubre que hay una conversión de int
a IntWrapper
través de su constructor de conversión, por lo que se utiliza este candidato.El objetivo de las expresiones sintetizadas es evitar el desorden de la necesidad de escribir el repetitivo de las funciones de
friend
para completar los espacios en los que su objeto podría convertirse de otros tipos. Las expresiones sintetizadas se generalizan a 0 @ (b <=> a)
.Tipos más complejos
El operador de nave espacial generado por el compilador no se detiene en miembros individuales de clases, generará un conjunto correcto de comparaciones para todos los subobjetos dentro de sus tipos:
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); }
El compilador sabe cómo expandir miembros de clases que son matrices en sus listas de subobjetos y compararlos recursivamente. Por supuesto, si desea escribir los cuerpos de estas funciones usted mismo, aún tiene el beneficio de las expresiones de reescritura del compilador para usted.
Parece un pato, nada como un pato y grazna como operator==
Algunas personas muy inteligentes en el comité de estandarización notaron que el operador de la nave espacial siempre realizará una comparación lexicográfica de elementos sin importar qué. La realización incondicional de comparaciones lexicográficas puede conducir a un código generado ineficiente con el operador de igualdad en particular.
El ejemplo canónico es comparar dos cadenas. Si tiene la cadena
"foobar"
y la compara con la cadena "foo"
usando == uno esperaría que la operación sea casi constante. El algoritmo eficiente de comparación de cadenas es así:- Primero compare el tamaño de las dos cadenas, si los tamaños difieren devuelven
false
, de lo contrario - recorra cada elemento de las dos cadenas al unísono y compare hasta que una difiera o se llegue al final, devuelva el resultado.
Según las reglas del operador de la nave espacial, primero debemos comenzar con la comparación profunda de cada elemento hasta encontrar el que es diferente. En nuestro ejemplo de
"foobar"
y "foo"
solo al comparar 'b'
con '\0'
finalmente devuelve false
.Para combatir esto, había un documento, P1185R2 que detalla una forma para que el compilador reescriba y genere el
operator==
independientemente del operador de la nave espacial. Nuestro IntWrapper
podría escribirse de la siguiente manera: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator==(const IntWrapper&) const = default; };
Solo un paso más ... sin embargo, hay buenas noticias; en realidad no necesita escribir el código anterior, ya que simplemente escribir el
auto operator<=>(const IntWrapper&) const = default
es suficiente para que el compilador genere implícitamente el operator==
por separado y más eficiente para usted.El compilador aplica una regla de "reescritura" ligeramente modificada específica para
==
y !=
Donde en estos operadores se reescriben en términos de operator==
y no operator<=>
. Esto significa que !=
También se beneficia de la optimización, también.El viejo código no se romperá
En este punto, podría estar pensando, OK, si el compilador puede realizar este negocio de reescritura del operador, ¿qué sucede cuando intento burlarme del compilador?
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 respuesta aquí es que no lo hiciste. El modelo de resolución de sobrecarga en C ++ tiene esta arena donde todos los candidatos luchan, y en esta batalla específica tenemos 3 candidatos:
IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)
(reescrito)
IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)
(sintetizado)
Si aceptamos las reglas de resolución de sobrecarga en C ++ 17, el resultado de esa llamada habría sido ambiguo, pero las reglas de resolución de sobrecarga de C ++ 20 se cambiaron para permitir que el compilador resuelva esta situación a la sobrecarga más lógica.
Hay una fase de resolución de sobrecarga en la que el compilador debe realizar una serie de desempates. En C ++ 20, hay un nuevo desempate que establece que debemos preferir las sobrecargas que no se reescriben o sintetizan, esto hace que nuestra sobrecarga
IntWrapper::operator<
el mejor candidato y resuelva la ambigüedad. Esta misma maquinaria evita que los candidatos sintetizados pisoteen expresiones regulares reescritas.Pensamientos finales
El operador de nave espacial es una adición bienvenida a C ++ y es una de las características que simplificará y ayudará a escribir menos código y, a veces, menos es más. ¡Así que abróchate el cinturón con el operador de nave espacial C ++ 20!
Le instamos a salir y probar el operador de la nave espacial, ¡está disponible ahora mismo en Visual Studio 2019 en
/std:c++latest
! Como nota, los cambios introducidos a través de P1185R2 estarán disponibles en Visual Studio 2019 versión 16.2. Tenga en cuenta que el operador de la nave espacial es parte de C ++ 20 y está sujeto a algunos cambios hasta el momento en que se finalice C ++ 20.Como siempre, agradecemos sus comentarios. No dude en enviar cualquier comentario por correo electrónico a visualcpp@microsoft.com , a través de Twitter @visualc o Facebook en Microsoft Visual Cpp . Además, siéntase libre de seguirme en Twitter @starfreakclone .
Si encuentra otros problemas con MSVC en VS 2019, infórmenos a través de la opción Informar un problema , ya sea desde el instalador o desde el IDE de Visual Studio. Para sugerencias o informes de errores, háganos saber a través de DevComm.