suma resta primaria parentesis para operaciones números multiplicación matematicas fracciones enteros ejercicios división con como combinadas c++ memcmp

c++ - resta - ¿Debo usar memcmp o operaciones encadenadas iguales cuando ambas dan el mismo resultado?



operaciones con fracciones (6)

Requisito : considere una clase o estructura T , que para dos objetos a y b de tipo T

memcmp(&a, &b, sizeof(T)) == 0

produce el mismo resultado que

a.member1 == b.member1 && a.member2 == b.member2 && ...

( memberN es una variable miembro no estática de T ).

Pregunta : ¿Cuándo debería memcmp para comparar a y b para la igualdad, y cuándo debería usarse == s encadenado?

Aquí hay un ejemplo simple:

struct vector { int x, y; };

Para sobrecargar el operador == para el vector , hay dos posibilidades (si se garantiza que darán el mismo resultado):

bool operator==(vector lhs, vector rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; }

o

bool operator==(vector lhs, vector rhs) { return memcmp(&lhs, &rhs, sizeof(vector)) == 0; }

Ahora, si un nuevo miembro se agregara a vector , por ejemplo, un componente z :

  • Si se utilizara == s para implementar el operator== , tendría que ser modificado.
  • Si se utilizara memcmp su lugar, el operator== no tendría que ser modificado en absoluto.

Pero creo que usar == s encadenados transmite un significado más claro. Aunque para una T grande con muchos miembros memcmp es más tentador. Además, ¿hay una mejora en el rendimiento al usar memcmp sobre == s? ¿Algo más a tener en cuenta?


Con respecto a la condición previa de memcmp produce el mismo resultado que las comparaciones de miembros con == , aunque esta condición previa se cumple a menudo en la práctica, es algo frágil .

Cambiar los compiladores o las opciones del compilador puede, en teoría, romper esa condición previa. Más preocupante es que el mantenimiento del código (y el 80% de todo el trabajo de programación es de mantenimiento, IIRC) puede romperlo agregando o eliminando miembros, haciendo que la clase sea polimórfica, agregando sobrecargas personalizadas == , etc. Y como se menciona en uno de los comentarios, la condición previa puede ser válida para las variables estáticas mientras que no lo es para las variables automáticas, y luego el trabajo de mantenimiento que crea objetos no estáticos puede hacer Bad Things ™.

Y con respecto a la cuestión de si usar memcmp o el uso de miembros == para implementar un operador == para la clase, primero, esta es una falsa dicotomía, ya que esas no son las únicas opciones.

Por ejemplo, puede ser menos trabajo y más fácil mantener la generación automática de sobrecargas de operadores relacionales , en términos de una función de compare . La función std::string::compare es un ejemplo de dicha función.

En segundo lugar, la respuesta a qué implementación elegir depende en gran medida de lo que considere importante, por ejemplo:

  • ¿Debería uno buscar maximizar la eficiencia del tiempo de ejecución , o

  • ¿Debería uno buscar crear el código más claro , o

  • ¿Debería uno buscar el código más terso, más rápido para escribir , o

  • ¿Debería uno buscar hacer la clase más segura de usar, o

  • otra cosa, tal vez?

Generando operadores relacionales.

Es posible que hayas oído hablar de CRTP, el patrón de plantilla curiosamente recurrente . Como recuerdo, se inventó para tratar el requisito de generar sobrecargas de operadores relacionales. Sin embargo, posiblemente pueda estar confundiendo eso con otra cosa, pero de todos modos:

template< class Derived > struct Relops_from_compare { friend auto operator!=( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) != 0; } friend auto operator<( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) < 0; } friend auto operator<=( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) <= 0; } friend auto operator==( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) == 0; } friend auto operator>=( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) >= 0; } friend auto operator>( const Derived& a, const Derived& b ) -> bool { return compare( a, b ) > 0; } };

Dado el soporte anterior, podemos investigar las opciones disponibles para su pregunta.

Implementación A: comparación por resta.

Esta es una clase que proporciona un conjunto completo de operadores relacionales sin utilizar memcmp o == :

struct Vector : Relops_from_compare< Vector > { int x, y, z; // This implementation assumes no overflow occurs. friend auto compare( const Vector& a, const Vector& b ) -> int { if( const auto r = a.x - b.x ) { return r; } if( const auto r = a.y - b.y ) { return r; } return a.z - b.z; } Vector( const int _x, const int _y, const int _z ) : x( _x ), y( _y ), z( _z ) {} };

Implementación B: comparación vía memcmp .

Esta es la misma clase implementada usando memcmp ; Creo que estarás de acuerdo en que este código se escala mejor y es más simple:

struct Vector : Relops_from_compare< Vector > { int x, y, z; // This implementation requires that there is no padding. // Also, it doesn''t deal with negative numbers for < or >. friend auto compare( const Vector& a, const Vector& b ) -> int { static_assert( sizeof( Vector ) == 3*sizeof( x ), "!" ); return memcmp( &a, &b, sizeof( Vector ) ); } Vector( const int _x, const int _y, const int _z ) : x( _x ), y( _y ), z( _z ) {} };

Implementación C: comparación miembro por miembro.

Esta es una implementación que usa comparaciones de miembros. No impone ningún requisito especial o suposiciones. Pero es más código fuente.

struct Vector : Relops_from_compare< Vector > { int x, y, z; friend auto compare( const Vector& a, const Vector& b ) -> int { if( a.x < b.x ) { return -1; } if( a.x > b.x ) { return +1; } if( a.y < b.y ) { return -1; } if( a.y > b.y ) { return +1; } if( a.z < b.z ) { return -1; } if( a.z > b.z ) { return +1; } return 0; } Vector( const int _x, const int _y, const int _z ) : x( _x ), y( _y ), z( _z ) {} };

Implementación D: compare en términos de operadores relacionales.

Este es un tipo de implementación que invierte el orden natural de las cosas, al implementar la compare en términos de < y == , que se proporcionan directamente y se implementan en términos de comparaciones std::tuple (usando std::tie ).

struct Vector { int x, y, z; friend auto operator<( const Vector& a, const Vector& b ) -> bool { using std::tie; return tie( a.x, a.y, a.z ) < tie( b.x, b.y, b.z ); } friend auto operator==( const Vector& a, const Vector& b ) -> bool { using std::tie; return tie( a.x, a.y, a.z ) == tie( b.x, b.y, b.z ); } friend auto compare( const Vector& a, const Vector& b ) -> int { return (a < b? -1 : a == b? 0 : +1); } Vector( const int _x, const int _y, const int _z ) : x( _x ), y( _y ), z( _z ) {} };

Como se indica, el código de cliente que usa, por ejemplo, necesita un using namespace std::rel_ops; .

Las alternativas incluyen agregar todos los otros operadores a lo anterior (mucho más código), o usar un esquema de generación de operadores CRTP que implemente a los otros operadores en términos de < y = (posiblemente de manera ineficiente).

Implementación E: comparación mediante el uso manual de < y == .

Esta implementación es el resultado de no aplicar ninguna abstracción, solo golpear el teclado y escribir directamente lo que debe hacer la máquina:

struct Vector { int x, y, z; friend auto operator<( const Vector& a, const Vector& b ) -> bool { return ( a.x < b.x || a.x == b.x && ( a.y < b.y || a.y == b.y && ( a.z < b.z ) ) ); } friend auto operator==( const Vector& a, const Vector& b ) -> bool { return a.x == b.x && a.y == b.y && a.z == b.z; } friend auto compare( const Vector& a, const Vector& b ) -> int { return (a < b? -1 : a == b? 0 : +1); } Vector( const int _x, const int _y, const int _z ) : x( _x ), y( _y ), z( _z ) {} };

Qué elegir.

Teniendo en cuenta la lista de posibles aspectos para valorar más, como seguridad, claridad, eficiencia, falta, evalúe cada uno de los enfoques anteriores.

Luego, elija el que sea mejor para usted, o uno de los enfoques que parezcan mejores.

Guía: Por seguridad, no querría elegir el enfoque A, la resta, ya que se basa en una suposición acerca de los valores. Tenga en cuenta que también la opción B, memcmp , es insegura como una implementación para el caso general, pero puede funcionar bien solo con == y != . Para mejorar la eficiencia, debe MEDIR , con las opciones y el entorno relevantes del compilador, y recordar el adagio de Donald Knuth: "la optimización prematura es la raíz de todo mal" (es decir, pasar tiempo en eso puede ser contraproducente).


Si dos soluciones son correctas, prefiera la más legible. Yo diría que para un programador de C ++, == es más legible que memcmp . Iría tan lejos como para usar std::tie lugar de encadenar:

bool operator==(const vector &lhs, const vector &rhs) { return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); }


Si hay alguna, solo si la estructura es POD y si es segura memcmp comparable (ni siquiera todos los tipos numéricos son ...) el resultado es el mismo y la pregunta es sobre la legibilidad y el rendimiento.

¿Legibilidad? Esta es una pregunta más bien basada en la opinión, pero creo que prefiero el operator== .

¿Actuación? operator== es un operador de cortocircuito. Usted tiene más control sobre su programa aquí porque puede reordenar la secuencia de comparación.

Aunque a == b && c == d y c == d && a == b son equivalentes en términos de lógica algorítmica (el resultado es el mismo) no son equivalentes en términos de ensamblaje producido, "lógica de fondo" y posiblemente actuación.

Puede influir en su programa si puede ver algunos puntos.

Por ejemplo:

  • Si ambas declaraciones tienen la misma probabilidad de producir falsas, primero querrá tener la declaración más barata para omitir la comparación más compleja, si es posible.
  • Si ambas afirmaciones son más o menos complejas y sabe de antemano que c == d es más probable que sea falso que a == b , primero debe comparar d .

Es posible ajustar la secuencia de comparación de una manera dependiente del problema usando operator== mientras que memcmp no le da este tipo de libertad.

PD: desearía medirlo, pero para una estructura pequeña con 3 miembros, MS VS 2013 produce un ensamblaje un poco más complejo para el caso memcmp . Espero que la solución del operator== tenga un mejor rendimiento (si el impacto fuera medible) en este caso.

- / edith-

Nota: Incluso los miembros de la estructura POD pueden tener un operator== sobrecargado operator== .

Considerar:

#include <iostream> #include <iomanip> struct A { int * p; }; bool operator== (A const &a, A const &b) { return *(a.p) == *(b.p); } struct B { A m; }; bool operator== (B const &a, B const &b) { return a.m == b.m; } int main() { int a(1), b(1); B x, y; x.m.p = &a; y.m.p = &b; std::cout << std::boolalpha; std::cout << (memcmp(&x, &y, sizeof(B)) == 0) << "/n"; std::cout << (x == y) << "/n"; return 0; }

Huellas dactilares

false true

Incluso si, a su vez, todos los miembros son tipos fundamentales, preferiría el operator== y dejar que el compilador considere la optimización de la comparación en cualquier ensamblaje que considere preferible.


Si, como usted dice, ha elegido tipos tales que las dos soluciones producen los mismos resultados (probablemente, entonces, no tiene datos indirectos y la alineación / el relleno son todos iguales), entonces claramente puede usar la solución que desee. .

Cosas para considerar:

  1. Rendimiento: dudo que veas mucha o ninguna diferencia, pero mídelo para estar seguro, si te importa;
  2. Seguridad: Bueno, dices que las dos soluciones son las mismas para tu T , pero ¿lo son? ¿Son realmente ? ¿En todos los sistemas? ¿Es tu enfoque memcmp portátil? Probablemente no;
  3. Claridad: si sus condiciones previas cambian y no hizo un comentario adecuado para describir su uso de memcmp , entonces su programa puede romperse: por lo tanto, lo ha hecho frágil;
  4. Consistencia: Presumiblemente usas == otros lugares; ciertamente tendrá que hacerlo por cada T que no cumpla con sus condiciones previas; a menos que esta sea una especialización deliberada de optimización para T , puede considerar apegarse a un solo enfoque a lo largo de su programa;
  5. Facilidad de uso: por supuesto, es bastante fácil perder a un miembro de == encadenado, especialmente si su lista de miembros crece.

Usted impuso una condición muy fuerte de que no hay relleno (no asumo ni entre los miembros de la clase, ni dentro de estos miembros). Supongo que también pretendía excluir de la clase los datos de mantenimiento "ocultos". Además, la pregunta en sí misma implica que siempre comparamos objetos del mismo tipo. Bajo condiciones tan fuertes, probablemente no haya manera de encontrar un contraejemplo que haga que la comparación basada en memcmp difiera de la comparación == .

Ya sea que valga la pena usar memcmp por razones de rendimiento ... bueno, si realmente tiene una buena razón para optimizar de manera agresiva algunas partes críticas de código y perfiles, demuestre que hay mejoras después de cambiar de == a memcmp , entonces siga adelante . Pero no lo usaría como una técnica de rutina para escribir operadores de comparación, incluso si su clase cumple con los requisitos.


== es mejor, porque memcmp compara datos de memoria pura (comparar de esa manera puede ser incorrecto en muchas situaciones, como std::string , clases que imitan matrices o tipos que pueden ser iguales incluso si no son perfectamente idénticos). Ya que dentro de sus clases puede haber tales tipos, siempre debe usar sus propios operadores en lugar de comparar datos de memoria sin procesar.

== también es mejor porque es más legible que alguna función de aspecto extraño.