¿Por qué se prueba la desigualdad como (! (A == b)) en muchos códigos de biblioteca estándar de C ++?

142

Este es el código del código de la biblioteca estándar de C ++ remove. ¿Por qué se prueba la desigualdad en if (!(*first == val))lugar de if (*first != val)?

 template <class ForwardIterator, class T>
      ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
 {
     ForwardIterator result = first;
     while (first!=last) {
         if (!(*first == val)) {
             *result = *first;
             ++result;
         }
         ++first;
     }
     return result;
 }
Ahmed Nawar
fuente
2
@BeyelerStudios es probablemente correcto. También es común cuando se implementa operator!=. Solo usa la operator==implementación:bool operator!=(const Foo& other) { return !(*this == other); }
simon
1
en realidad corrijo mi declaración: la mención de referencias elimina todos los elementos que son iguales al valor, por lo que operator==se espera que se use aquí ...
BeyelerStudios
Ah, y también debería haber un constejemplo en mi comentario anterior, pero entiendes el punto. (Demasiado tarde para editarlo)
Simon
La razón de esto está relacionada con otra pregunta (que básicamente puede responderse con "No, no necesariamente"), y el concepto de ser EqualityComparableque Hurkyl mencionó en su respuesta .
Marco13

Respuestas:

144

Porque esto significa que el único requisito en T es implementar un operator==. Podría requerir que T tenga una, operator!=pero la idea general aquí es que debe poner la menor carga posible en el usuario de la plantilla y otras plantillas sí lo necesitan operator==.

Tom Tanner
fuente
13
template <class T> inline bool operator! = <T a, T b> {return! (a == b); }
Joshua
8
¿Habría algún escenario en el que un compilador no pudiera intercambiar todas las instancias de =! a! (==)? ¿Por qué esta no sería la acción predeterminada que debe tomar el compilador?
Aidan Gómez
20
@AidanGomez Para bien o para mal, puede sobrecargar a los operadores para que hagan lo que quieran. No tiene que tener sentido o ser coherente.
Neil Kirk
10
x != yno se define para ser lo mismo que !(x == y). ¿Qué sucede si estos operadores devuelven el árbol de análisis de un DSL incorporado?
Brice M. Dempsey
77
@Joshua Eso se rompe mal si intenta usar SFINAE para detectar si !=es compatible (incorrectamente devolvería verdadero, ¡incluso si operator==no es compatible!). También me preocupa que pueda causar que algunos usos !=sean ambiguos.
36

La mayoría de las funciones en STL solo funcionan con operator<o operator==. Esto requiere que el usuario solo implemente estos dos operadores (o, a veces, al menos uno de ellos). Por ejemplo, std::setutiliza operator<(más precisamente, std::lessqué invoca operator<por defecto) y no operator>para gestionar pedidos. La removeplantilla en su ejemplo es un caso similar: usa solo operator==y no operator!=así operator!=no necesita ser definido.

Lukáš Bednařík
fuente
2
Las funciones no se usan operator<directamente, sino que se usan std::less, lo que a su vez es predeterminado operator<.
Christian Hackl
1
En realidad, parece que las funciones del algoritmo estándar, a diferencia de std::set, por ejemplo , se usan operator<directamente. Extraño ...
Christian Hackl
1
Estas funciones no se usan std::equal_to, se usan operator==como se indica en la pregunta. La situación con std::lesses similar. Bueno, tal vez std::setno sea el mejor ejemplo.
Lukáš Bednařík
2
@ChristianHackl, std::equal_toy std::lessse utilizan como parámetros de plantilla predeterminados donde el comparador se toma como parámetro. operator==y operator<se usan directamente donde se requiere el tipo para satisfacer la igualdad de ordenamiento débil comparable y estricto respectivamente, por ejemplo, iteradores e iteradores de acceso aleatorio.
Jan Hudec
28

Este es el código de la biblioteca estándar de C ++ que elimina el código.

Incorrecto. No es elremove código de la biblioteca estándar de C ++ . Es una posible implementación interna de la removefunción de biblioteca estándar de C ++ . El estándar C ++ no prescribe el código real; Prescribe prototipos de funciones y comportamientos requeridos.

En otras palabras: desde un punto de vista estrictamente lingüístico, el código que está viendo no existe . Puede ser de algún archivo de encabezado que viene con la implementación de la biblioteca estándar de su compilador. Tenga en cuenta que el estándar C ++ ni siquiera requiere que existan esos archivos de encabezado . Los archivos son solo una forma conveniente para que los implementadores del compilador cumplan con los requisitos para una línea como #include <algorithm>(es decir, hacer std::removey otras funciones disponibles).

¿Por qué se prueba la desigualdad en if (!(*first == val))lugar de if (*first != val)?

Porque solo operator==es requerido por la función.

Cuando se trata de la sobrecarga del operador para tipos personalizados, el idioma le permite hacer todo tipo de cosas raras. Muy bien podría crear una clase que tenga una sobrecargada operator==pero no sobrecargada operator!=. O peor aún: podría sobrecargarse operator!=pero hacer que haga cosas completamente ajenas.

Considere este ejemplo:

#include <algorithm>
#include <vector>

struct Example
{
    int i;

    Example() : i(0) {}

    bool operator==(Example const& other) const
    {
        return i == other.i;
    }

    bool operator!=(Example const& other) const
    {
        return i == 5; // weird, but nothing stops you
                       // from doing so
    }

};

int main()
{
  std::vector<Example> v(10);
  // ...
  auto it = std::remove(v.begin(), v.end(), Example());
  // ...
}

Si se std::removeusa operator!=, el resultado sería bastante diferente.

Christian Hackl
fuente
1
Otra cosa a considerar es que puede ser posible para ambos a==by a!=bpara devolver falso. Si bien puede que no siempre esté claro si dicha situación se consideraría de manera más significativa como "igual" o "no igual", una función que define la igualdad únicamente en términos del operador "==" debe considerarlos como "no igual" ", independientemente de qué comportamiento tendría más sentido [si tuviera mis druthers, se esperaría que todos los tipos hagan que los operadores" == "y"! = "con rendimiento booleano se comporten de manera consistente, pero el hecho de que IEEE-754 exige la igualdad rota los operadores dificultarían tal expectativa].
supercat
18
"Desde un punto de vista estrictamente lingüístico, el código que está viendo no existe". - cuando un punto de vista dice que algo no existe, pero en realidad estás mirando esa cosa, entonces el punto de vista es incorrecto. De hecho, el estándar no dice que el código no existe, simplemente no dice que existe :-)
Steve Jessop
@SteveJessop: puede reemplazar la expresión "desde un punto de vista estrictamente del lenguaje" con algo como "estrictamente, en el nivel del lenguaje". El punto es que el código publicado por el OP no es asunto del estándar de idioma.
Christian Hackl
2
@supercat: IEEE-754 hace ==y se !=comporta de manera consistente, aunque siempre he pensado que las seis relaciones deberían evaluar falsecuándo es al menos un operando NaN.
Ben Voigt
@BenVoigt: Ah, eso es correcto. Hace que los dos operadores se comporten de la misma manera quebrada, de modo que sean consistentes entre sí, pero aún así infrinjan todos los demás axiomas normales asociados con la equivalencia (por ejemplo, no defienden ni a == a, ni la garantía de que las operaciones se realizaron en valores iguales producirá resultados iguales).
supercat
15

Algunas buenas respuestas aquí. Solo quería agregar una pequeña nota.

Como todas las buenas bibliotecas, la biblioteca estándar está diseñada con (al menos) dos principios muy importantes en mente:

  1. Ponga la menor cantidad de responsabilidad en los usuarios de su biblioteca con la que pueda salirse con la suya. Parte de esto tiene que ver con darles la menor cantidad de trabajo para hacer al usar su interfaz. (como definir la menor cantidad de operadores posible). La otra parte tiene que ver con no sorprenderlos o exigirles que verifiquen los códigos de error (así que mantenga las interfaces consistentes y arroje excepciones <stdexcept>cuando las cosas salgan mal).

  2. Eliminar toda la redundancia lógica . Todas las comparaciones pueden deducirse simplemente de operator<, entonces, ¿por qué exigir que los usuarios definan a otros? p.ej:

    (a> b) es equivalente a (b <a)

    (a> = b) es equivalente a! (a <b)

    (a == b) es equivalente a! ((a <b) || (b <a))

    y así.

    Por supuesto, en esta nota, uno podría preguntarse por qué unordered_maprequiere operator==(al menos por defecto) en lugar de operator<. La respuesta es que en una tabla hash, la única comparación que necesitamos es una para la igualdad. Por lo tanto, es lógicamente más consistente (es decir, tiene más sentido para el usuario de la biblioteca) exigirles que definan un operador de igualdad. Requerir un operator<sería confuso porque no es inmediatamente obvio por qué lo necesitarías.

Richard Hodges
fuente
10
Con respecto a su segundo punto: hay tipos que lógicamente se pueden comparar para la igualdad incluso si no existe un orden lógico, o para los cuales establecer un orden sería muy artificial. Por ejemplo, el rojo es rojo y el rojo no es verde, pero ¿es el rojo inherentemente menos que el verde?
Christian Hackl
1
Completamente de acuerdo. Uno no almacenaría estos artículos en un contenedor ordenado porque no hay un orden lógico. Podrían almacenarse más adecuadamente en un contenedor desordenado que requiere operator==(y hash).
Richard Hodges
3. Sobrecargue los operadores menos estándar como sea posible. Esta es otra razón que implementaron !(a==b). Debido a que la sobrecarga irreflexiva de los operadores puede hacer que el programa C ++ se desordene por completo (además, hacer que el programador se vuelva loco porque depurar su código puede convertirse en una misión imposible, ya que encontrar al culpable de un error en particular se parece a una odisea).
syntaxerror
!((a < b) || (b < a))usa un operador bool menos, por lo que probablemente sea más rápido
Filip Haglund
1
En realidad no lo hacen. En lenguaje ensamblador, todas las comparaciones se implementan como una resta seguida de pruebas de acarreo y cero bits en el registro de banderas. Todo lo demás es solo azúcar sintáctico.
Richard Hodges
8

El EqualityComparableconcepto solo requiere que operator==se defina.

En consecuencia, cualquier función que profese trabajar con tipos satisfactorios EqualityComparable no puede depender de la existencia de operator!=objetos de ese tipo. (a menos que existan requisitos adicionales que impliquen la existencia de operator!=).


fuente
1

El enfoque más prometedor es encontrar un método para determinar si se puede llamar al operador == para un tipo particular, y luego apoyarlo solo cuando esté disponible; en otras situaciones, se lanzaría una excepción. Sin embargo, hasta la fecha no hay una forma conocida de detectar si una expresión de operador arbitraria f == g está adecuadamente definida. La mejor solución conocida tiene las siguientes cualidades indeseables:

  • Falla en tiempo de compilación para objetos donde el operador == no es accesible (por ejemplo, porque es privado).
  • Falla en tiempo de compilación si llamar al operador == es ambiguo.
  • Parece correcto si la declaración de operador == es correcta, aunque el operador == no pueda compilar.

De Boost FAQ: fuente

Sabiendo que requerir una ==implementación es una carga , nunca querrás crear una carga adicional al requerir!= implementación también.

Para mí personalmente, se trata de la parte L SÓLIDA (diseño orientado a objetos): principio de sustitución de Liskov: "los objetos en un programa deben ser reemplazables con instancias de sus subtipos sin alterar la corrección de ese programa". En este caso, es el operador ! = Que puedo reemplazar con == y inversa booleana en lógica booleana.

Margus
fuente