C ++ 20 introdujo comparaciones predeterminadas, también conocidas como "nave espacial"operator<=>
, que le permite solicitar operadores <
/ <=
/ ==
/ !=
/ >=
/ y / o >
operadores generados por el compilador con la implementación obvia / ingenua (?) ...
auto operator<=>(const MyClass&) const = default;
... pero puede personalizarlo para situaciones más complicadas (discutidas a continuación). Vea aquí la propuesta de lenguaje, que contiene justificaciones y discusión. Esta respuesta sigue siendo relevante para C ++ 17 y versiones anteriores, y para obtener información sobre cuándo debe personalizar la implementación de operator<=>
....
Puede parecer un poco inútil que C ++ no haya estandarizado esto antes, pero a menudo las estructuras / clases tienen algunos miembros de datos para excluir de la comparación (por ejemplo, contadores, resultados almacenados en caché, capacidad del contenedor, código de éxito / error de la última operación, cursores), como así como decisiones a tomar acerca de innumerables cosas que incluyen, entre otras:
- qué campos comparar primero, por ejemplo, comparar un
int
miembro en particular podría eliminar el 99% de los objetos desiguales muy rápidamente, mientras que un map<string,string>
miembro a menudo puede tener entradas idénticas y ser relativamente costoso de comparar - si los valores se cargan en tiempo de ejecución, el programador puede tener información sobre el el compilador no puede posiblemente
- al comparar cadenas: sensibilidad a mayúsculas y minúsculas, equivalencia de espacios en blanco y separadores, convenciones de escape ...
- precisión al comparar flotadores / dobles
- si los valores de coma flotante de NaN deben considerarse iguales
- comparando punteros o apuntados a datos (y si es el último, cómo saber si los punteros son a matrices y cuántos objetos / bytes necesitan comparación)
- si el orden importa cuando se comparan contenedores sin clasificar (por ejemplo
vector
, list
), y si es así, si está bien ordenarlos en el lugar antes de comparar o usar memoria adicional para clasificar los temporales cada vez que se realiza una comparación
- cuántos elementos de la matriz contienen actualmente valores válidos que deben compararse (¿hay un tamaño en alguna parte o un centinela?)
- que miembro de
union
a comparar
- normalización: por ejemplo, los tipos de fecha pueden permitir un día del mes o un mes del año fuera de rango, o un objeto racional / fracción puede tener 6/8 mientras que otro tiene 3/4, que por razones de rendimiento corrigen perezosamente con un paso de normalización separado; es posible que deba decidir si desencadenar una normalización antes de la comparación
- qué hacer cuando los punteros débiles no son válidos
- cómo manejar miembros y bases que no se implementan por
operator==
sí mismos (pero pueden tener compare()
o operator<
o str()
o captadores ...)
- qué bloqueos se deben tomar al leer / comparar datos que otros hilos pueden querer actualizar
Por lo tanto, es bueno tener un error hasta que haya pensado explícitamente en lo que debería significar la comparación para su estructura específica, en lugar de dejar que se compile pero no le dé un resultado significativo en tiempo de ejecución .
Dicho todo esto, sería bueno si C ++ le permitiera decir bool operator==() const = default;
cuándo decidió que una ==
prueba "ingenua" miembro por miembro estaba bien. Lo mismo para !=
. Dadas las múltiples miembros / bases, "por defecto" <
, <=
, >
, y >=
las implementaciones parecen sin esperanza, aunque - en cascada sobre la base de la orden de que todo es posible, pero muy poco probable que sea lo que quería, dado conflictivos imperativos para el pedido miembro (bases de ser necesariamente ante los miembros, la agrupación por declaración accesibilidad, construcción / destrucción antes del uso dependiente). Para ser más útil, C ++ necesitaría un nuevo miembro de datos / sistema de anotación de base para guiar las elecciones; sin embargo, eso sería una gran cosa para tener en el Estándar, idealmente junto con la generación de código definido por el usuario basada en AST ... eso'
Implementación típica de operadores de igualdad
Una implementación plausible
Es probable que una implementación razonable y eficiente sea:
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return lhs.my_struct2 == rhs.my_struct2 &&
lhs.an_int == rhs.an_int;
}
Tenga en cuenta que esto necesita una operator==
para MyStruct2
también.
Las implicaciones de esta implementación y las alternativas se discuten bajo el título Discusión de los detalles de su MyStruct1 a continuación.
Un enfoque coherente para ==, <,> <= etc.
Es fácil aprovechar std::tuple
los operadores de comparación para comparar sus propias instancias de clase; solo utilícelos std::tie
para crear tuplas de referencias a campos en el orden de comparación deseado. Generalizando mi ejemplo desde aquí :
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return std::tie(lhs.my_struct2, lhs.an_int) ==
std::tie(rhs.my_struct2, rhs.an_int);
}
inline bool operator<(const MyStruct1& lhs, const MyStruct1& rhs)
{
return std::tie(lhs.my_struct2, lhs.an_int) <
std::tie(rhs.my_struct2, rhs.an_int);
}
// ...etc...
Cuando "posee" (es decir, puede editar, un factor con las bibliotecas corporativas y de terceros) la clase que desea comparar, y especialmente con la preparación de C ++ 14 para deducir el tipo de retorno de función de la return
declaración, a menudo es mejor agregar un " empate "función miembro a la clase que desea poder comparar:
auto tie() const { return std::tie(my_struct1, an_int); }
Entonces las comparaciones anteriores se simplifican a:
inline bool operator==(const MyStruct1& lhs, const MyStruct1& rhs)
{
return lhs.tie() == rhs.tie();
}
Si desea un conjunto más completo de operadores de comparación, sugiero aumentar los operadores (buscar less_than_comparable
). Si no es adecuado por alguna razón, es posible que le guste o no la idea de macros de soporte (en línea) :
#define TIED_OP(STRUCT, OP, GET_FIELDS) \
inline bool operator OP(const STRUCT& lhs, const STRUCT& rhs) \
{ \
return std::tie(GET_FIELDS(lhs)) OP std::tie(GET_FIELDS(rhs)); \
}
#define TIED_COMPARISONS(STRUCT, GET_FIELDS) \
TIED_OP(STRUCT, ==, GET_FIELDS) \
TIED_OP(STRUCT, !=, GET_FIELDS) \
TIED_OP(STRUCT, <, GET_FIELDS) \
TIED_OP(STRUCT, <=, GET_FIELDS) \
TIED_OP(STRUCT, >=, GET_FIELDS) \
TIED_OP(STRUCT, >, GET_FIELDS)
... que luego se puede usar a la ...
#define MY_STRUCT_FIELDS(X) X.my_struct2, X.an_int
TIED_COMPARISONS(MyStruct1, MY_STRUCT_FIELDS)
(Versión de enlace de miembros de C ++ 14 aquí )
Discusión de los detalles de su MyStruct1
Hay implicaciones en la elección de proporcionar un miembro independiente versus un miembro operator==()
...
Implementación independiente
Tienes una decisión interesante que tomar. Como su clase se puede construir implícitamente a partir de a MyStruct2
, una función independiente / no miembro bool operator==(const MyStruct2& lhs, const MyStruct2& rhs)
admitiría ...
my_MyStruct2 == my_MyStruct1
... primero creando un temporal MyStruct1
desde my_myStruct2
, luego haciendo la comparación. Esto definitivamente dejaría MyStruct1::an_int
establecido el valor del parámetro predeterminado del constructor de -1
. Dependiendo de si se incluyen an_int
comparación en la implementación de su operator==
, una MyStruct1
podría o no podría comparar igual a una MyStruct2
que sí se compara igual a la MyStruct1
's my_struct_2
miembro! Además, la creación de un temporal MyStruct1
puede ser una operación muy ineficaz, ya que implica copiar el my_struct2
miembro existente en un temporal, solo para desecharlo después de la comparación. (Por supuesto, puede evitar esta construcción implícita de MyStruct1
s para la comparación haciendo ese constructor explicit
o eliminando el valor predeterminado para an_int
).
Implementación de miembros
Si desea evitar la construcción implícita de a MyStruct1
desde a MyStruct2
, convierta el operador de comparación en una función miembro:
struct MyStruct1
{
...
bool operator==(const MyStruct1& rhs) const
{
return tie() == rhs.tie(); // or another approach as above
}
};
Tenga en cuenta que la const
palabra clave, solo necesaria para la implementación del miembro, le advierte al compilador que comparar objetos no los modifica, por lo que se puede permitir en const
objetos.
Comparando las representaciones visibles
A veces, la forma más fácil de obtener el tipo de comparación que desea puede ser ...
return lhs.to_string() == rhs.to_string();
... que a menudo también es muy caro, ¡esos que se string
crean dolorosamente solo para tirarlos! Para los tipos con valores de punto flotante, comparar representaciones visibles significa que el número de dígitos mostrados determina la tolerancia dentro de la cual los valores casi iguales se tratan como iguales durante la comparación.
struct
s para la igualdad? Y si desea la forma más sencilla, siempre haymemcmp
tanto tiempo que sus estructuras no contienen puntero.