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

302

Soy un gran admirador de dejar que el compilador haga el mayor trabajo posible por usted. Al escribir una clase simple, el compilador puede darle lo siguiente de forma gratuita:

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

Pero parece que no puede darle ningún operador de comparación, como operator==o 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 un problema realizar una comparación miembro por miembro? Obviamente, si la clase asigna memoria, entonces querrás tener cuidado, pero para una clase simple, ¿seguramente el compilador podría hacer esto por ti?

Robar
fuente
44
Por supuesto, también el destructor se proporciona de forma gratuita.
Johann Gerell
23
En una de sus conversaciones recientes, Alex Stepanov señaló que fue un error no tener una automática predeterminada ==, de la misma manera que hay una asignación automática predeterminada ( =) bajo ciertas condiciones. (El argumento sobre los punteros es inconsistente porque la lógica se aplica tanto para =y ==, y no solo para el segundo).
alfC
2
@becko Es uno de la serie en A9: youtube.com/watch?v=k-meLQaYP5Y , no recuerdo en cuál de las conversaciones. También hay una propuesta de que parece estar llegando a C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC
1
@becko, es uno de los primeros en la serie "Programación eficiente con componentes" o "Conversaciones de programación", ambas en A9, disponibles en Youtube.
alfC
1
@becko En realidad, hay una respuesta a continuación que apunta al punto de vista de Alex stackoverflow.com/a/23329089/225186
alfC

Respuestas:

71

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

Es más seguro no implementarlo y dejar que el programador lo haga por sí mismo. Luego pueden hacer todas las suposiciones que deseen.

Mark Ingram
fuente
293
Ese problema no impide que genere un copiador, donde es bastante dañino.
MSalters
78
Los constructores de copias (y operator=) generalmente funcionan en el mismo contexto que los operadores de comparación, es decir, existe la expectativa de que, después de realizar a = b, a == bsea ​​cierto. Definitivamente tiene sentido que el compilador proporcione un valor predeterminado operator==utilizando la misma semántica de valor agregado que para operator=. Sospecho que paercebal es realmente correcto aquí, ya que operator=(y el copiador) se proporcionan únicamente por compatibilidad con C, y no querían empeorar la situación.
Pavel Minaev
46
-1. Por supuesto que desea una comparación profunda, si el programador desea una comparación de puntero, escribiría (& f1 == & f2)
Viktor Sehr
62
Viktor, te sugiero que reconsideres tu respuesta. Si la clase Foo contiene una barra *, ¿cómo podría saber el compilador si Foo :: operator == quiere comparar la dirección de la barra * o el contenido de la barra?
Mark Ingram
46
@Mark: si contiene un puntero, la comparación de los valores del puntero es razonable; si contiene un valor, la comparación de los valores es razonable. En circunstancias excepcionales, el programador podría anular. Esto es como el lenguaje implementa la comparación entre ints y puntero a ints.
Eamon Nerbonne
317

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

Personalmente considero desafortunado que las operaciones de copia estén definidas por defecto y prohíbo la copia de objetos de muchas de mis clases. Sin embargo, C ++ heredó su asignación predeterminada y los constructores de copia de C, y se usan con frecuencia.

Entonces, en lugar de "¿por qué C ++ no tiene un valor 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 Stroustrup incluyó esos elementos de mala gana para la compatibilidad con C (probablemente la causa de la mayoría de las verrugas de C ++, pero también probablemente la razón principal de la popularidad de C ++).

Para mis propios fines, en mi IDE, el fragmento que uso para las nuevas clases contiene declaraciones para un operador de asignación privado y un constructor de copia, de modo que cuando genero una nueva clase no obtengo operaciones predeterminadas de asignación y copia: tengo que eliminar explícitamente la declaración de esas operaciones de la private:sección si quiero que el compilador pueda generarlas por mí.

Michael Burr
fuente
29
Buena respuesta. Solo me gustaría señalar que en C ++ 11, en lugar de hacer que el operador de asignación y el constructor de copia sean privados, puede eliminarlos por completo de esta manera: Foo(const Foo&) = delete; // no copy constructoryFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc
99
"Sin embargo, C ++ heredó su asignación predeterminada y los constructores de copia de C" Eso no implica por qué tiene que hacer TODOS los tipos de C ++ de esta manera. Deberían haber restringido esto a PODs viejos y simples, solo los tipos que ya están en C, no más.
thesaint
3
Ciertamente puedo entender por qué C ++ heredó estos comportamientos struct, pero deseo que deje de classcomportarse de manera diferente (y sensata). En el proceso, también habría dado una diferencia más significativa entre structy classal lado del acceso predeterminado.
jamesdlin 01 de
@jamesdlin Si desea una regla, deshabilitar la declaración implícita y la definición de ctors y la asignación si se declara un dtor tendría más sentido.
Deduplicador
1
Todavía no veo ningún daño en dejar programador para pedir explícitamente al compilador para crear una operator==. En este punto, es solo azúcar de sintaxis para algún código de placa de caldera. Si tiene miedo de que de esta manera el programador pueda pasar por alto algún puntero entre los campos de clase, puede agregar una condición de que solo puede funcionar en tipos primitivos y objetos que tienen operadores de igualdad. Sin embargo, no hay ninguna razón para no permitir esto por completo.
NO_NAME
93

Incluso en C ++ 20, el compilador aún no generará implícitamente operator==para usted

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

Pero va a adquirir la capacidad de forma explícita por defecto == ya que C ++ 20 :

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

El valor predeterminado se ==realiza en función de los miembros ==(del mismo modo que el constructor de copias predeterminado realiza la construcción de copias en función de los miembros). Las nuevas reglas también proporcionan la relación esperada entre ==y !=. Por ejemplo, con la declaración anterior, puedo escribir ambos:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

Esta característica específica (valor predeterminado operator==y simetría entre ==y !=) proviene de una propuesta que era parte de la característica de lenguaje más amplia que es operator<=>.

Anton Savin
fuente
¿Sabes si hay alguna actualización más reciente sobre esto? ¿Va a estar disponible en c ++ 17?
dcmm88
3
@ dcmm88 Desafortunadamente no estará disponible en C ++ 17. He actualizado la respuesta.
Anton Savin el
2
Sin embargo, una propuesta modificada que permite lo mismo (excepto la forma abreviada) estará en C ++ 20 :)
Rakete1111
Entonces, básicamente, debe especificar = default, para lo que no se crea de forma predeterminada, ¿verdad? A mí me parece un oxímoron ("valor predeterminado explícito").
artin
@artin Tiene sentido ya que agregar nuevas funciones al lenguaje no debería romper la implementación existente. Agregar nuevos estándares de biblioteca o cosas nuevas que el compilador puede hacer es una cosa. Agregar nuevas funciones de miembro donde no existían anteriormente es una historia completamente diferente. Asegurar su proyecto de errores requeriría mucho más esfuerzo. Personalmente, prefiero el indicador del compilador para cambiar entre el valor predeterminado explícito e implícito. Construye un proyecto a partir del estándar C ++ anterior, usa el valor predeterminado explícito por el indicador del compilador. Ya actualizas el compilador, por lo que debes configurarlo correctamente. Para nuevos proyectos hacerlo implícito.
Maciej Załucki
44

En mi humilde opinión, no hay una "buena" razón. 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. Las personas necesitan escribir muchos constructores de copias personalizadas, operadores de comparación y destructores porque usan punteros sin procesar en su implementación.

Cuando se utilizan punteros inteligentes apropiados (como std :: shared_ptr), el constructor de copia predeterminado generalmente está bien y la implementación obvia del operador de comparación por defecto hipotético sería tan buena.

alexk7
fuente
39

Se respondió que C ++ no hizo == porque C no lo hizo, y esta es la razón por la cual C proporciona solo default = pero no == en 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 está inicializado, memcmp dice que son diferentes a pesar de que son 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. Algún ejemplo de código con respecto a esto. Su corrección es apreciada si me equivoco.

Rio Wing
fuente
66
C ++ no usa memcpy para operator=, eso solo funcionaría para los tipos de POD, pero C ++ también proporciona un valor predeterminado operator=para los tipos que no son POD.
Flexo
2
Sí, C ++ implementado = de una manera más sofisticada. Parece que C acaba de implementarse = con una memoria simple.
Rio Wing
El contenido de esta respuesta se debe combinar con el de Michael. Su corrige la pregunta y luego esto la responde.
Sgene9
27

En esto video Alex Stepanov, el creador de STL aborda esta misma pregunta aproximadamente a las 13:00. En resumen, después de observar la evolución de C ++, argumenta que:

  • Es lamentable que == y! = No se declaren implícitamente (y Bjarne está de acuerdo con él). Un lenguaje correcto debería tener esas cosas listas para usted (va más allá y sugiere que no debería ser capaz de definir a ! = Que rompa la semántica de == )
  • La razón de que este sea el caso tiene sus raíces (como muchos de los problemas de C ++) en C. Allí, el operador de asignación se define implícitamente con la asignación de bit a bit, pero eso no funcionaría para == . Se puede encontrar una explicación más detallada en este artículo de Bjarne Stroustrup.
  • En la pregunta de seguimiento ¿ Por qué entonces no se utilizó una comparación miembro por miembro? se , dice algo sorprendente : C era una especie de lenguaje de cosecha propia y el tipo que implementaba estas cosas para Ritchie le dijo que le resultaba difícil de implementar!

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

Nikos Athanasiou
fuente
2
Parece que este futuro lejano no será 2017 ni 18, ni 19, bueno, me
entiendes
18

C ++ 20 proporciona una manera 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 implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>
vll
fuente
44
Me sorprende que se utilizan Pointcomo un ejemplo para un pedido operación, ya que no hay manera predeterminada razonable para dos puntos con xy ycoordenadas ...
tubería
44
@pipe Si no le importa en qué orden están los elementos, tiene sentido usar el operador predeterminado. Por ejemplo, puede usar std::setpara asegurarse de que todos los puntos sean únicos y solo los std::setuse operator<.
vll
Acerca del tipo de devolución auto: para este caso, ¿podemos suponer siempre que será std::strong_orderingde #include <compare>?
kevinarpe
1
@kevinarpe El tipo de retorno es std::common_comparison_category_t, que para esta clase se convierte en el orden predeterminado ( std::strong_ordering).
vll
15

No es posible definir el valor predeterminado ==, pero puede definir el valor predeterminado !=mediante el ==cual generalmente debería definirse a sí mismo. Para esto debes hacer lo siguiente:

#include <utility>
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 usastd::rel_ops .

Pero debes tener cuidado cuando uses std::rel_ops porque los operadores de comparación se pueden deducir para los tipos para los que no se espera.

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

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

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

sergtk
fuente
No obtuve el valor predeterminado !=después de escribir el ==operador. O lo hice pero le faltaba constness. Tuve que escribirlo yo también y todo estuvo bien.
John
puedes jugar con constancia para lograr los resultados necesarios. Sin código es difícil decir qué tiene de malo.
sergtk
2
Hay una razón que rel_opsquedó en desuso en C ++ 20: porque no funciona , al menos no en todas partes, y ciertamente no de manera consistente. No hay una forma confiable sort_decreasing()de compilar. Por otro lado, Boost.Operators funciona y siempre ha funcionado.
Barry
10

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

MSalters
fuente
3
Pensé que solo las "funciones miembro especiales" (constructor predeterminado, constructor de copia, operador de asignación y destructor) podrían ser explícitamente predeterminadas. ¿Han extendido esto a otros operadores?
Michael Burr
44
El constructor de movimiento también puede ser predeterminado, pero no creo que esto se aplique a operator==. Lo cual es una pena.
Pavel Minaev
5

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

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

Paul de Vrieze
fuente
10
No hay ambigüedad para las estructuras de POD: deben comportarse exactamente de la misma manera que cualquier otro tipo de POD, que es la igualdad de valores (en lugar de la igualdad de referencia). Uno intcreado a través de copiador de otro es igual al que se creó; lo único lógico que hacer para uno structde los dos intcampos es trabajar exactamente de la misma manera.
Pavel Minaev
1
@mgiuca: Puedo ver una utilidad considerable para una relación de equivalencia universal que permitiría que cualquier tipo que se comporta como un valor se use como clave en un diccionario o colección similar. Sin embargo, tales colecciones no pueden comportarse de manera útil sin una relación de equivalencia reflexiva garantizada. En mi humilde opinión, la mejor solución sería definir un nuevo operador que todos los tipos incorporados pudieran implementar con sensatez, y definir algunos nuevos tipos de punteros que fueran similares a los existentes, excepto que algunos definirían la igualdad como equivalencia de referencia mientras que otros encadenarían al objetivo operador de equivalencia.
supercat
1
@supercat Por analogía, podría hacer casi el mismo argumento para el +operador en que no es asociativo para flotantes; eso es (x + y) + z! = x + (y + z), debido a la forma en que se produce el redondeo de FP. (Podría decirse que este es un problema mucho peor que ==porque es cierto para los valores numéricos normales). Podría sugerir agregar un nuevo operador de suma que funcione para todos los tipos numéricos (incluso int) y sea casi exactamente igual +pero asociativo ( de algun modo). Pero entonces estaría agregando hinchazón y confusión al lenguaje sin realmente ayudar a tanta gente.
mgiuca
1
@mgiuca: Tener cosas que son bastante similares, excepto en casos extremos, a menudo es extremadamente útil, y los esfuerzos equivocados para evitar tales cosas resultan en una complejidad innecesaria. Si el código del cliente a veces necesita que los casos extremos se manejen de una manera, y a veces necesita que se manejen de otra manera, tener un método para cada estilo de manejo eliminará una gran cantidad de código de manejo de casos extremos en el cliente. En cuanto a su analogía, no hay forma de definir la operación en valores de punto flotante de tamaño fijo para producir resultados transitivos en todos los casos (aunque algunos idiomas de la década de 1980 tenían una semántica mejor ...
supercat
1
... que el de hoy en ese sentido) y, por lo tanto, el hecho de que no hagan lo imposible no debería ser una sorpresa. Sin embargo, no existe un obstáculo fundamental para implementar una relación de equivalencia que sea universalmente aplicable a cualquier tipo de valor que pueda copiarse.
supercat
1

¿Hay alguna buena razón para esto? ¿Por qué sería un problema 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 se puede omitir el resto. Entonces, si hay algunos miembros que generalmente son iguales, desea compararlos en último lugar, y el compilador no sabe qué miembros tienen más probabilidades de ser iguales.

Considere este ejemplo, donde verboseDescriptionse selecciona una cadena larga 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 compilador tendría derecho a ignorar el orden de las comparaciones si reconoce que no tienen efectos secundarios, pero presumiblemente aún tomaría su cola del código fuente donde no tiene mejor información propia).

Musculoso
fuente
Pero nadie le impide escribir una comparación optimizada definida por el usuario si encuentra un problema de rendimiento. En mi experiencia, eso sería una minúscula minoría de casos.
Peter - Restablece a Monica el
1

Solo para que las respuestas a esta pregunta permanezcan completas a medida que pasa el tiempo: desde C ++ 20 se puede generar automáticamente con el comando auto operator<=>(const foo&) const = default;

Generará todos los operadores: ==,! =, <, <=,> Y> =, consulte https://en.cppreference.com/w/cpp/language/default_comparisons para más detalles.

Debido a la apariencia del operador <=>, se llama operador de nave espacial. Vea también ¿Por qué necesitamos el operador de nave espacial <=> en C ++?.

EDIT: También en C ++ 11 un sustituto ordenada bastante para que esté disponible con std::tiever https://en.cppreference.com/w/cpp/utility/tuple/tie para un ejemplo de código completo con bool operator<(…). La parte interesante, cambiada para trabajar ==es:

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie funciona con todos los operadores de comparación y el compilador lo optimiza por completo.

cosurgi
fuente
-1

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

Una vez tuve un caso POD 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 comparó con la carga útil, algo que el compilador nunca entendería o podría resolver por sí mismo.

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

graham.reeds
fuente