¿Por qué los comstackdores de C ++ no definen operator == y operator! =?

Soy un gran admirador de dejar que el comstackdor haga tanto trabajo como sea posible. Al escribir una clase simple, el comstackdor puede darle lo siguiente para ‘gratis’:

  • Un constructor predeterminado (vacío)
  • Un constructor de copia
  • Un destructor
  • Un operador de asignación ( operator= )

Pero parece que no puede proporcionarle operadores de comparación, como operator== u operator!= . Por ejemplo:

 class foo { public: std::string str_; int n_; }; foo f1; // Works foo f2(f1); // Works foo f3; f3 = f2; // Works if (f3 == f2) // Fails { } if (f3 != f2) // Fails { } 

¿Hay alguna buena razón para esto? ¿Por qué sería problemático realizar una comparación miembro por miembro? Obviamente, si la clase asigna memoria, entonces debería tener cuidado, pero para una clase simple seguramente el comstackdor podría hacer esto por usted.

El comstackdor no sabría si quería una comparación de puntero o una comparación profunda (interna).

Es más seguro simplemente no implementarlo y dejar que el progtwigdor lo haga por sí mismo. Entonces pueden hacer todas las suposiciones que les gustan.

El argumento de que si el comstackdor puede proporcionar un constructor de copia predeterminado, debería ser capaz de proporcionar un operator==() predeterminado similar operator==() tiene un cierto sentido. Creo que el motivo de la decisión de no proporcionar un valor predeterminado generado por el comstackdor para este operador puede adivinarse por lo que Stroustrup dijo sobre el constructor de copia predeterminado en “El diseño y la evolución de C ++” (Sección 11.4.1 – Control de copia) :

Personalmente, considero desafortunado que las operaciones de copia se definan por defecto y prohibo copiar objetos de muchas de mis clases. Sin embargo, C ++ heredó su asignación predeterminada y copió los constructores de C, y se utilizan con frecuencia.

Entonces, en lugar de “¿por qué C ++ no tiene un operator==() predeterminado operator==() ?”, La pregunta debería haber sido “¿por qué C ++ tiene una asignación predeterminada y un constructor de copia?”, Con la respuesta de que esos elementos se incluyeron a regañadientes Stroustrup por compatibilidad con C (probablemente la causa de la mayoría de las verrugas de C ++, pero también es probablemente la razón principal de la popularidad de C ++).

Para mis propios fines, en mi IDE, el fragmento que uso para nuevas clases contiene declaraciones para un operador de asignación privado y un constructor de copia para que cuando genere una nueva clase no obtenga operaciones predeterminadas de asignación y copia – Debo eliminar explícitamente la statement de esas operaciones desde la sección private: si quiero que el comstackdor pueda generarlas para mí.

ACTUALIZACIÓN 2: Desafortunadamente esta propuesta no llegó a C ++ 17 , por lo que nada está cambiando en el lenguaje con respecto a esto por ahora.

ACTUALIZACIÓN: la versión actual de la propuesta que tiene muchas posibilidades de ser votado en C ++ 17 está aquí .

Hay una propuesta reciente (N4126) sobre operadores de comparación explícitamente predeterminados, que ha recibido comentarios muy positivos del comité estándar, por lo que esperamos verlo de alguna forma en C ++ 17.

En resumen, la syntax propuesta es:

 struct Thing { int a, b, c; std::string d; }; bool operator==(const Thing &, const Thing &)= default; bool operator!=(const Thing &, const Thing &)= default; 

O en forma de friend para las clases con campos privados:

 class Thing { int a, b; friend bool operator<(Thing, Thing) = default; friend bool operator>(Thing, Thing) = default; friend bool operator<=(Thing, Thing) = default; friend bool operator>=(Thing, Thing) = default; }; 

O incluso en forma corta:

 struct Thing { int a, b, c; std::string d; default: ==, !=, <, >, <=, >=; // defines the six non-member functions }; 

Por supuesto, todo esto puede cambiar cuando esta propuesta sea finalmente aceptada.

En mi humilde opinión, no hay una razón “buena”. La razón por la que hay tantas personas que están de acuerdo con esta decisión de diseño es porque no aprendieron a dominar el poder de la semántica basada en el valor. La gente necesita escribir un montón de constructores de copia personalizada, operadores de comparación y destructores porque usan punteros sin procesar en su implementación.

Al usar punteros inteligentes apropiados (como std :: shared_ptr), el constructor de copia predeterminado suele ser correcto y la implementación obvia del operador hipotético de comparación por defecto sería igual de buena.

Se responde que C ++ no funcionó == porque C no lo hizo, y aquí está la razón por la cual C solo proporciona el valor predeterminado = pero no == en el primer lugar. C quería mantenerlo simple: C implementado = por memcpy; sin embargo, == no puede ser implementado por memcmp debido al relleno. Debido a que el relleno no se inicializa, memcmp dice que son diferentes aunque sean iguales. El mismo problema existe para la clase vacía: memcmp dice que son diferentes porque el tamaño de las clases vacías no es cero. Se puede ver desde arriba que implementar == es más complicado que implementar = en C. Algunos ejemplos de código con respecto a esto. Su corrección es apreciada si estoy equivocado.

En este video, Alex Stepanov, el creador de STL, aborda esta misma pregunta alrededor de las 13:00. Para resumir, al observar la evolución de C ++, argumenta que:

  • Es desafortunado que == y! = No estén declarados implícitamente (y Bjarne está de acuerdo con él). Un lenguaje correcto debería tener esas cosas preparadas para usted (va más allá y sugiere que no debería poder definir un ! = Que rompa la semántica de == )
  • La razón por la cual este es el caso tiene sus raíces (como muchos problemas de C ++) en C. Allí, el operador de asignación se define implícitamente con asignación bit a bit, pero eso no funcionaría para == . Una explicación más detallada se puede encontrar en este artículo de Bjarne Stroustrup.
  • En la pregunta de seguimiento ¿ Por qué entonces no se utilizó una comparación miembro por miembro? Dice algo asombroso : C era un tipo de lenguaje propio y el chico que implementó estas cosas para Ritchie le dijo que descubrió que esto era difícil de implementar.

Luego dice que en el futuro (distante) == y ! = Se generará implícitamente.

No es posible definir el valor predeterminado == , pero puede definir el valor predeterminado != Via == que generalmente debería definir usted mismo. Para esto deberías hacer lo siguiente:

 #include  using namespace std::rel_ops; ... class FooClass { public: bool operator== (const FooClass& other) const { // ... } }; 

Puede ver http://www.cplusplus.com/reference/std/utility/rel_ops/ para más detalles.

Además, si define operator< , los operadores para <=,>,> = pueden deducirse de él cuando se usa std::rel_ops .

Pero debe tener cuidado cuando use std::rel_ops porque los operadores de comparación se pueden deducir para los tipos que no se esperan.

La forma más preferida de deducir operador relacionado del básico es usar boost :: operadores .

El enfoque utilizado en el impulso es mejor porque define el uso del operador para la clase que solo desea, no para todas las clases en el scope.

También puede generar "+" desde "+ =", - desde "- =", etc ... (vea la lista completa aquí )

C ++ 0x ha tenido una propuesta para funciones predeterminadas, por lo que podría decir default operator==; Hemos aprendido que ayuda a hacer que estas cosas sean explícitas.

Solo una nota, también proporcionada por el comstackdor de forma gratuita:

  • operador nuevo
  • operador nuevo []
  • operador borrar
  • operador delete []

Conceptualmente, no es fácil definir la igualdad. Incluso para los datos POD, se podría argumentar que incluso si los campos son los mismos, pero es un objeto diferente (en una dirección diferente), no es necesariamente igual. Esto en realidad depende del uso del operador. Lamentablemente, su comstackdor no es psíquico y no puede inferir eso.

Además de esto, las funciones predeterminadas son excelentes maneras de dispararse en el pie. Los valores predeterminados que describe están básicamente ahí para mantener la compatibilidad con las estructuras POD. Sin embargo, causan esgulps más que suficientes con los desarrolladores olvidándose de ellos, o la semántica de las implementaciones predeterminadas.

C ++ 20 proporciona una forma de implementar fácilmente un operador de comparación predeterminado.

Ejemplo de cppreference.com :

 class Point { int x; int y; public: auto operator<=>(const Point&) const = default; // ... non-comparison functions ... }; // compiler generates all six relational operators Point pt1, pt2; if (pt1 == pt2) { /*...*/ } // ok std::set s; // ok s.insert(pt1); // ok if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to <=> 

Estoy de acuerdo, para las clases de tipo POD, entonces el comstackdor podría hacerlo por usted. Sin embargo, lo que podría considerar simple, el comstackdor podría equivocarse. Entonces, es mejor dejar que el progtwigdor lo haga.

Tuve un caso POD una vez donde dos de los campos eran únicos, por lo que una comparación nunca se consideraría verdadera. Sin embargo, la comparación que necesitaba solo se comparaba con la carga útil, algo que el comstackdor nunca entendería o que nunca podría descubrir por sí mismo.

Además, no tardan mucho en escribir ¿verdad?

¿Hay alguna buena razón para esto? ¿Por qué sería problemático realizar una comparación miembro por miembro?

Puede que no sea un problema funcional, pero en términos de rendimiento, la comparación predeterminada miembro por miembro puede ser más subóptima que la asignación / copia predeterminada miembro por miembro. A diferencia del orden de asignación, el orden de comparación afecta el rendimiento porque el primer miembro desigual implica que el rest se puede omitir. Entonces, si hay algunos miembros que generalmente son iguales, usted quiere compararlos al último, y el comstackdor no sabe qué miembros tienen más probabilidades de ser iguales.

Considere este ejemplo, donde verboseDescription es una cadena larga seleccionada de un conjunto relativamente pequeño de descripciones meteorológicas posibles.

 class LocalWeatherRecord { std::string verboseDescription; std::tm date; bool operator==(const LocalWeatherRecord& other){ return date==other.date && verboseDescription==other.verboseDescription; // The above makes a lot more sense than // return verboseDescription==other.verboseDescription // && date==other.date; // because some verboseDescriptions are liable to be same/similar } } 

(Por supuesto, el comstackdor tendría derecho a ignorar el orden de las comparaciones si reconoce que no tienen efectos secundarios, pero presumiblemente tomaría su cola del código fuente donde no tiene mejor información propia).

Los operadores de comparación predeterminados serían correctos una cantidad de tiempo que se angosta. Espero que sean una fuente de problemas en lugar de algo útil.

Además, los métodos predeterminados que mencionas a menudo son indeseables. Ver código como este para deshacerse del constructor de copia predeterminado y operator = es muy común:

 class NonAssignable { // .... private: NonAssignable(const NonAssignable&); // Unimplemented NonAssignable& operator=(const NonAssignable&); // Unimplemented }; 

En una gran cantidad de código, es común ver un comentario “constructor de copia predeterminado y operador = OK” para indicar que no es un error que hayan sido eliminados o definidos explícitamente.