¿Son == y! = Mutuamente dependientes?

292

Estoy aprendiendo sobre la sobrecarga de operadores en C ++, y veo eso ==y !=son simplemente algunas funciones especiales que se pueden personalizar para los tipos definidos por el usuario. Sin embargo, mi preocupación es, ¿por qué se necesitan dos definiciones separadas ? Pensé que si a == bes verdad, entonces a != bes automáticamente falso, y viceversa, y no hay otra posibilidad, porque, por definición, lo a != bes !(a == b). Y no podía imaginar ninguna situación en la que esto no fuera cierto. ¿Pero quizás mi imaginación es limitada o soy ignorante de algo?

Sé que puedo definir uno en términos del otro, pero esto no es lo que estoy preguntando. Tampoco estoy preguntando sobre la distinción entre comparar objetos por valor o por identidad. O si dos objetos podrían ser iguales y no iguales al mismo tiempo (¡esto definitivamente no es una opción! Estas cosas son mutuamente excluyentes). Lo que pregunto es esto:

¿Hay alguna situación posible en la que hacer preguntas acerca de que dos objetos sean iguales no tiene sentido, pero preguntar si no son iguales no tiene sentido? (ya sea desde la perspectiva del usuario o desde la perspectiva del implementador)

Si no existe tal posibilidad, ¿por qué en la Tierra C ++ tiene estos dos operadores definidos como dos funciones distintas?

BarbaraKwarc
fuente
13
Dos punteros pueden ser nulos pero no necesariamente iguales.
Ali Caglayan
2
No estoy seguro si tiene sentido aquí, pero leer esto me hizo pensar en problemas de 'cortocircuito'. Por ejemplo, uno podría definir que 'undefined' != expressionsiempre es verdadero (o falso o indefinido), independientemente de si la expresión puede evaluarse. En este caso a!=b, devolvería el resultado correcto según la definición, pero !(a==b)fallaría si bno se puede evaluar. (O tome mucho tiempo si la evaluación bes costosa).
Dennis Jaheruddin
2
¿Qué pasa con nulo! = Nulo y nulo == nulo? Pueden ser ambos ... así que si a! = B eso no siempre significa a == b.
zozo
44
Un ejemplo de javascript(NaN != NaN) == true
chiliNUT

Respuestas:

272

Usted no desea que el idioma de reescribir automáticamente a != bcomo !(a == b)cuando se a == bvuelve algo más que una bool. Y hay algunas razones por las que podrías hacer que lo haga.

Es posible que tenga objetos de creación de expresiones, donde a == bno se pretende y no se pretende realizar ninguna comparación, sino que simplemente crea algún nodo de expresión que representa a == b.

Es posible que tenga una evaluación diferida, donde a == bno se pretende y no se pretende realizar ninguna comparación directamente, sino que se devuelve algún tipo de lazy<bool>eso que se puede convertir boolimplícita o explícitamente en algún momento posterior para realizar la comparación. Posiblemente combinado con los objetos del generador de expresiones para permitir la optimización completa de la expresión antes de la evaluación.

Puede tener alguna optional<T>clase de plantilla personalizada , donde se le dan variables opcionales ty udesea permitir t == u, pero hacer que regrese optional<bool>.

Probablemente hay más en lo que no pensé. Y aunque en estos ejemplos la operación a == by las a != bdos tienen sentido, todavía a != bno es lo mismo !(a == b), por lo que se necesitan definiciones separadas.


fuente
72
La construcción de expresiones es un fantástico ejemplo práctico de cuándo desearía esto, que no se basa en escenarios artificiales.
Oliver Charlesworth
66
Otro buen ejemplo serían las operaciones lógicas vectoriales. Prefiere una pasada por los datos de computación !=en lugar de dos pases de computación ==a continuación !. Especialmente en aquellos días en los que no podías confiar en el compilador para fusionar los bucles. O incluso hoy, si no logra convencer al compilador, sus vectores no se superponen.
41
"Usted puede tener la expresión constructor de objetos" - bueno, entonces el operador !también puede construir algún nodo de expresión y eso que todavía estamos reemplazando a != bcon !(a == b), en lo que va. Lo mismo ocurre lazy<bool>::operator!, puede volver lazy<bool>. optional<bool>es más convincente, ya que la veracidad lógica de, por ejemplo, boost::optionaldepende de si existe un valor, no del valor en sí mismo.
Steve Jessop
42
Todo eso, y Nans - por favor recuerde el NaNs;
jsbueno
99
@jsbueno: se ha señalado más abajo que los NaN no son especiales a este respecto.
Oliver Charlesworth
110

Si no existe tal posibilidad, ¿por qué en la Tierra C ++ tiene estos dos operadores definidos como dos funciones distintas?

Porque puedes sobrecargarlos, y al sobrecargarlos puedes darles un significado totalmente diferente al original.

Tomemos, por ejemplo, operador <<, originalmente el operador de desplazamiento a la izquierda bit a bit, ahora comúnmente sobrecargado como operador de inserción, como en std::cout << something; significado totalmente diferente del original.

Entonces, si acepta que el significado de un operador cambia cuando lo sobrecarga, entonces no hay razón para evitar que el usuario le dé un significado al operador ==que no sea exactamente la negación del operador !=, aunque esto puede ser confuso.

alcaudón
fuente
18
Esta es la única respuesta que tiene sentido práctico.
Sonic Atom
2
Para mí parece que tienes la causa y el efecto al revés. Puede sobrecargarlos por separado debido ==y !=existir como operadores distintos. Por otro lado, probablemente no existan como operadores distintos porque puede sobrecargarlos por separado, pero debido a razones de legado y conveniencia (brevedad del código).
nitro2k01
60

Sin embargo, mi preocupación es, ¿por qué se necesitan dos definiciones separadas?

No tienes que definir ambos.
Si son mutuamente excluyentes, aún puede ser conciso definiendo ==y <junto con std :: rel_ops

De la preferencia:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

¿Hay alguna situación posible en la que hacer preguntas acerca de que dos objetos sean iguales no tiene sentido, pero preguntar si no son iguales no tiene sentido?

A menudo asociamos estos operadores a la igualdad.
Aunque así es como se comportan en los tipos fundamentales, no hay obligación de que este sea su comportamiento en los tipos de datos personalizados. Ni siquiera tiene que devolver un bool si no lo desea.

He visto personas sobrecargar a los operadores de maneras extrañas, solo para descubrir que tiene sentido para su aplicación específica de dominio. Incluso si la interfaz parece mostrar que son mutuamente excluyentes, el autor puede querer agregar lógica interna específica.

(ya sea desde la perspectiva del usuario o desde la perspectiva del implementador)

Sé que quieres un ejemplo específico,
así que aquí hay uno del marco de prueba de Catch que pensé que era práctico:

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

Estos operadores están haciendo cosas diferentes, y no tendría sentido definir un método como un! (No) del otro. La razón por la que se hace esto es para que el marco pueda imprimir la comparación realizada. Para hacer eso, necesita capturar el contexto de qué operador sobrecargado se utilizó.

Trevor Hickey
fuente
14
Oh Dios mío, ¿cómo podría no saberlo std::rel_ops? Muchas gracias por señalarlo.
Daniel Jour
55
Las copias casi textuales de cppreference (o en cualquier otro lugar) deben marcarse claramente y atribuirse adecuadamente. rel_opsEs horrible de todos modos.
TC
@TC De acuerdo, solo digo que es un método que OP puede tomar. No sé cómo explicar rel_ops de manera más simple que el ejemplo que se muestra. Me vinculé a dónde está, pero publiqué el código ya que la página de referencia siempre podría cambiar.
Trevor Hickey
44
Todavía debe dejar en claro que el ejemplo de código es 99% de cppreference, en lugar de la suya.
TC
2
Std :: relops parece haber caído en desgracia. Echa un vistazo a las operaciones de impulso para algo más específico.
JDługosz
43

Hay algunas convenciones muy bien establecidas en la que (a == b)y (a != b)son ambas falsas no necesariamente opuestos. En particular, en SQL, cualquier comparación con NULL produce NULL, no verdadero o falso.

Probablemente no sea una buena idea crear nuevos ejemplos de esto si es posible, porque es muy poco intuitivo, pero si está intentando modelar una convención existente, es bueno tener la opción de hacer que sus operadores se comporten "correctamente" para eso contexto.

Jander
fuente
44
¿Implementando un comportamiento nulo similar a SQL en C ++? Ewwww. Pero supongo que no es algo que creo que deba prohibirse en el idioma, por desagradable que sea.
1
@ dan1111 Más importante aún, algunos tipos de SQL pueden codificarse en c ++, por lo que el lenguaje debe admitir su sintaxis, ¿no?
Joe
1
Corrígeme si me equivoco, solo salgo de wikipedia aquí, pero la comparación con un valor NULL en SQL no devuelve ¿Desconocido, no falso? ¿Y no es la negación de Desconocido todavía Desconocido? Entonces, si la lógica SQL se codificara en C ++, ¿no querría NULL == somethingdevolver Desconocido, y también desearía NULL != somethingdevolver Desconocido y desearía !Unknownregresar Unknown? Y en ese caso, la implementación operator!=como la negación de operator==sigue siendo correcta.
Benjamin Lindley
1
@Barmar: De acuerdo, pero ¿cómo hace eso para que la afirmación "SQL NULLs funcione de esta manera" sea correcta? Si estamos restringiendo nuestras implementaciones de operadores de comparación a booleanos de retorno, ¿no significa eso que implementar la lógica SQL con estos operadores es imposible?
Benjamin Lindley
2
@Barmar: Bueno, no, ese no es el punto. El OP ya conoce ese hecho, o esta pregunta no existiría. El punto era presentar un ejemplo en el que tenía sentido 1) implementar uno de operator==o operator!=, pero no el otro, o 2) implementar operator!=de una manera diferente a la negación de operator==. E implementar la lógica SQL para valores NULL no es un caso de eso.
Benjamin Lindley
23

Solo responderé la segunda parte de su pregunta, a saber:

Si no existe tal posibilidad, ¿por qué en la Tierra C ++ tiene estos dos operadores definidos como dos funciones distintas?

Una razón por la que tiene sentido permitir que el desarrollador sobrecargue ambos es el rendimiento. Puede permitir optimizaciones implementando ambos ==y !=. Entonces x != ypodría ser más barato de lo que !(x == y)es. Algunos compiladores pueden optimizarlo para usted, pero tal vez no, especialmente si tiene objetos complejos con muchas ramificaciones involucradas.

Incluso en Haskell, donde los desarrolladores se toman muy en serio las leyes y los conceptos matemáticos, uno puede sobrecargar ambos ==y /=, como puede ver aquí ( http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude .html # v: -61--61- ):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

Esto probablemente se consideraría micro-optimización, pero podría estar justificado en algunos casos.

Centril
fuente
3
Las clases de contenedor SSE (x86 SIMD) son un gran ejemplo de esto. Hay una pcmpeqbinstrucción, pero ninguna instrucción de comparación de paquetes produce una máscara! =. Entonces, si no puede revertir la lógica de lo que sea que use los resultados, debe usar otra instrucción para invertirlo. (Dato curioso: el conjunto de instrucciones XOP de AMD tiene una comparación compacta neq. Lástima que Intel no adoptó / extendió XOP; hay algunas instrucciones útiles en esa extensión ISA que pronto estará muerta).
Peter Cordes
1
El punto principal de SIMD en primer lugar es el rendimiento, y normalmente solo te molestas en usarlo manualmente en bucles que son importantes para el rendimiento general. Guardar una sola instrucción ( PXORcon todos para invertir el resultado de la máscara de comparación) en un ciclo cerrado puede ser importante.
Peter Cordes
El rendimiento como razón no es creíble cuando la sobrecarga es una negación lógica .
Saludos y hth. - Alf
Podría ser más de una negación lógica si la informática x == ycuesta más significativamente que x != y. Cálculo de la última podría ser significativamente más barato debido a la predicción de ramificación, etc.
Centril
16

¿Hay alguna situación posible en la que hacer preguntas acerca de que dos objetos sean iguales no tiene sentido, pero preguntar si no son iguales no tiene sentido? (ya sea desde la perspectiva del usuario o desde la perspectiva del implementador)

Esa es una opinion. Quizás no. Pero los diseñadores del lenguaje, al no ser omniscientes, decidieron no restringir a las personas que podrían presentar situaciones en las que podría tener sentido (al menos para ellos).

Benjamin Lindley
fuente
13

En respuesta a la edición;

Es decir, si es posible que algún tipo tenga el operador ==pero no el !=, o viceversa, y cuándo tiene sentido hacerlo.

En general , no, no tiene sentido. La igualdad y los operadores relacionales generalmente vienen en conjuntos. Si existe la igualdad, entonces también la desigualdad; menor que, luego mayor que y así sucesivamente con el <=etc. También se aplica un enfoque similar a los operadores aritméticos, que generalmente también vienen en conjuntos lógicos naturales.

Esto se evidencia en el std::rel_opsespacio de nombres. Si implementa los operadores de igualdad y menor que, el uso de ese espacio de nombres le proporciona los otros, implementados en términos de sus operadores implementados originales.

Dicho todo esto, ¿hay condiciones o situaciones en las que una no significaría inmediatamente la otra o no podría implementarse en términos de las otras? Sí, hay pocos, pero están allí; de nuevo, como se evidencia en rel_opsser un espacio de nombres propio. Por esa razón, permitir que se implementen de forma independiente le permite aprovechar el lenguaje para obtener la semántica que necesita o necesita de una manera que sigue siendo natural e intuitiva para el usuario o cliente del código.

La evaluación perezosa ya mencionada es un excelente ejemplo de esto. Otro buen ejemplo es darles semánticas que no significan igualdad o desigualdad en absoluto. Un ejemplo similar a esto son los operadores de desplazamiento de bits <<y >>se utilizan para la inserción y extracción de flujos. Aunque puede estar mal visto en círculos generales, en algunas áreas específicas del dominio puede tener sentido.

Niall
fuente
12

Si los operadores ==y !=no implican realmente igualdad, de la misma manera que los operadores <<y >>stream no implican desplazamiento de bits. Si trata los símbolos como si significaran algún otro concepto, no tienen que ser mutuamente excluyentes.

En términos de igualdad, podría tener sentido si su caso de uso garantiza tratar los objetos como no comparables, de modo que cada comparación devuelva falso (o un tipo de resultado no comparable, si sus operadores devuelven no bool). No puedo pensar en una situación específica en la que esto esté justificado, pero podría ver que es lo suficientemente razonable.

Taywee
fuente
7

Con un gran poder viene de manera responsable, o al menos muy buenas guías de estilo.

==y !=se puede sobrecargar para hacer lo que quieras. Es a la vez una bendición y una maldición. No hay garantía que !=signifique !(a==b).

EsPete
fuente
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

No puedo justificar la sobrecarga de este operador, pero en el ejemplo anterior es imposible definirlo operator!=como "opuesto" de operator==.

Dafang Cao
fuente
1
@Snowman: Dafang no dice que sea una buena enumeración (ni una buena idea para definir una enumeración como esa), es solo un ejemplo para ilustrar un punto. Con esta definición de operador (quizás mala), entonces !=no significaría lo contrario de ==.
AlainD
1
@AlainD ¿Hiciste clic en el enlace que publiqué y conoces el propósito de ese sitio? Esto se llama "humor".
1
@Snowman: Ciertamente lo hago ... lo siento, ¡perdí que fuera un enlace y pretendía ser una ironía! : o)
AlainD
Espera, ¿estás sobrecargando unario ==?
LF
5

Al final, lo que está comprobando con esos operadores es que la expresión a == bo a != bestá devolviendo un valor booleano ( trueo false). Estas expresiones devuelven un valor booleano después de la comparación en lugar de ser mutuamente excluyentes.

Anirudh Sohil
fuente
4

[..] ¿por qué se necesitan dos definiciones separadas?

Una cosa a tener en cuenta es que puede existir la posibilidad de implementar uno de estos operadores de manera más eficiente que simplemente usar la negación del otro.

(Mi ejemplo aquí fue basura, pero el punto sigue en pie, piense en los filtros de floración, por ejemplo: permiten una prueba rápida si algo no está en un conjunto, pero probar si está dentro puede llevar mucho más tiempo).

[..] por definición, a != bes !(a == b).

Y es su responsabilidad como programador hacer eso. Probablemente sea algo bueno para escribir un examen.

Daniel Jour
fuente
44
¿Cómo !((a == rhs.a) && (b == rhs.b))no permite cortocircuitos? si !(a == rhs.a), entonces (b == rhs.b)no será evaluado.
Benjamin Lindley
Sin embargo, este es un mal ejemplo. El cortocircuito no agrega ninguna ventaja mágica aquí.
Oliver Charlesworth
@Oliver Charlesworth Solo no lo hace, pero cuando se une con operadores separados, lo hace: en caso de ==que, deje de comparar tan pronto como los primeros elementos correspondientes no sean iguales. Pero en caso de que !=, si se implementara en términos de ==, tendría que comparar primero todos los elementos correspondientes (cuando son todos iguales) para poder decir que no son no iguales: P Pero cuando se implementa como en En el ejemplo anterior, dejará de compararse tan pronto como encuentre el primer par no igual. Gran ejemplo de hecho.
BarbaraKwarc
@BenjaminLindley Cierto, mi ejemplo fue una completa tontería. Desafortunadamente, no puedo encontrar otro cajero automático, es demasiado tarde aquí.
Daniel Jour
1
@BarbaraKwarc: !((a == b) && (c == d))y (a != b) || (c != d)son equivalentes en términos de eficiencia de cortocircuito.
Oliver Charlesworth
2

Al personalizar el comportamiento de los operadores, puede hacer que hagan lo que quiera.

Es posible que desee personalizar las cosas. Por ejemplo, es posible que desee personalizar una clase. Los objetos de esta clase se pueden comparar simplemente marcando una propiedad específica. Sabiendo que este es el caso, puede escribir un código específico que solo verifique las cosas mínimas, en lugar de verificar cada bit de cada propiedad en todo el objeto.

Imagine un caso en el que puede descubrir que algo es diferente tan rápido, si no más rápido, que puede descubrir que algo es igual. Por supuesto, una vez que descubres si algo es igual o diferente, entonces puedes saber lo contrario simplemente volteando un poco. Sin embargo, cambiar ese bit es una operación adicional. En algunos casos, cuando el código se vuelve a ejecutar mucho, guardar una operación (multiplicado por muchas veces) puede tener un aumento general de velocidad. (Por ejemplo, si guarda una operación por píxel de una pantalla de megapíxeles, entonces acaba de guardar un millón de operaciones. Multiplicado por 60 pantallas por segundo, y guarda aún más operaciones).

La respuesta de hvd proporciona algunos ejemplos adicionales.

TOOGAM
fuente
2

Sí, porque uno significa "equivalente" y otro significa "no equivalente" y estos términos son mutuamente excluyentes. Cualquier otro significado para estos operadores es confuso y debe evitarse por todos los medios.

oliora
fuente
No son mutuamente excluyentes para todos los casos. Por ejemplo, dos infinitos no iguales entre sí y no iguales entre sí.
vladon
@vladon puede usar use uno en lugar de otro en caso genérico ? No. Esto significa que simplemente no son iguales. Todo lo demás va a una función especial en lugar de al operador == /! =
oliora
@vladon por favor, en lugar de un caso genérico, lea todos los casos en mi respuesta.
oliora
@vladon Por mucho que esto sea cierto en matemáticas, ¿puedes dar un ejemplo donde a != bno sea igual !(a == b)por esta razón en C?
nitro2k01
2

Tal vez una regla incomparable, donde a != bera falso y a == bera falso como un bit sin estado.

if( !(a == b || a != b) ){
    // Stateless
}
ToñitoG
fuente
Si desea reorganizar los símbolos lógicos, entonces ([A] || [B]) lógicamente se convierte en ([! A] y [! B])
Thijser
Tenga en cuenta que el tipo de retorno de operator==()y operator!=()no son necesariamente bool, podrían ser una enumeración que incluya apátridas si así lo desea y, sin embargo, los operadores aún podrían definirse, así que se (a != b) == !(a==b)mantiene ..
lorro