Comparaciones firmadas / no firmadas

85

Estoy tratando de entender por qué el siguiente código no emite una advertencia en el lugar indicado.

//from limits.h
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define INT_MAX  2147483647 /* maximum (signed) int value */
            /* = 0x7fffffff */

int a = INT_MAX;
//_int64 a = INT_MAX; // makes all warnings go away
unsigned int b = UINT_MAX;
bool c = false;

if(a < b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a > b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a <= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a >= b) // warning C4018: '<' : signed/unsigned mismatch
    c = true;
if(a == b) // no warning <--- warning expected here
    c = true;
if(((unsigned int)a) == b) // no warning (as expected)
    c = true;
if(a == ((int)b)) // no warning (as expected)
    c = true;

Pensé que tenía que ver con la promoción de fondo, pero los dos últimos parecen decir lo contrario.

En mi opinión, ¿la primera ==comparación es tanto una falta de coincidencia firmada / no firmada como las demás?

Pedro
fuente
3
gcc 4.4.2 imprime advertencia cuando se invoca con '-Wall'
bobah
Esto es especulación, pero tal vez esté optimizando todas las comparaciones, ya que conoce la respuesta en el momento de la compilación.
Conjunto nulo
2
¡Ah! re. Comentario de bobah: activé todas las advertencias y ahora aparece la advertencia que falta. Soy de la opinión de que debería haber aparecido en el mismo nivel de advertencia que las otras comparaciones.
Peter
1
@bobah: Realmente odio que gcc 4.4.2 imprima esa advertencia (sin forma de decirle que solo la imprima por desigualdad), ya que todas las formas de silenciar esa advertencia empeoran las cosas . La promoción predeterminada convierte de manera confiable -1 o ~ 0 en el valor más alto posible de cualquier tipo sin firmar, pero si silencia la advertencia lanzándola usted mismo, debe saber el tipo exacto . Entonces, si cambia el tipo (extiéndalo, diga a unsigned long long), sus comparaciones con bare -1seguirán funcionando (pero dan una advertencia) mientras que sus comparaciones con -1uo (unsigned)-1ambos fallarán miserablemente.
Jan Hudec
No sé por qué necesita una advertencia y por qué los compiladores simplemente no pueden hacer que funcione. -1 es negativo, por lo que es menor que cualquier número sin signo. Simples.
CashCow

Respuestas:

95

Al comparar firmado con no firmado, el compilador convierte el valor firmado en no firmado. Para la igualdad, esto no importa -1 == (unsigned) -1. Para otras comparaciones que importe, por ejemplo, lo siguiente es cierto: -1 > 2U.

EDITAR: Referencias:

5/9: (Expresiones)

Muchos operadores binarios que esperan operandos de tipo aritmético o enumeración provocan conversiones y producen tipos de resultados de manera similar. El propósito es producir un tipo común, que también es el tipo de resultado. Este patrón se denomina conversiones aritméticas habituales, que se definen de la siguiente manera:

  • Si alguno de los operandos es de tipo long double, el otro se convertirá en long double.

  • De lo contrario, si alguno de los operandos es doble, el otro se convertirá en doble.

  • De lo contrario, si alguno de los operandos es flotante, el otro se convertirá en flotante.

  • De lo contrario, las promociones integrales (4.5) se realizarán en ambos operandos. 54)

  • Entonces, si alguno de los operandos tiene un signo largo, el otro se convertirá en un operador largo sin signo.

  • De lo contrario, si un operando es un int largo y el otro int sin signo, entonces si un int largo puede representar todos los valores de un int sin signo, el int sin signo se convertirá en un int largo; de lo contrario, ambos operandos se convertirán a unsigned long int.

  • De lo contrario, si alguno de los operandos es largo, el otro se convertirá en largo.

  • De lo contrario, si alguno de los operandos está sin firmar, el otro se convertirá a unsigned.

4.7 / 2: (Conversiones integrales)

Si el tipo de destino no tiene signo, el valor resultante es el menor entero sin signo congruente con el entero de origen (módulo 2 n donde n es el número de bits utilizados para representar el tipo sin signo). [Nota: En una representación en complemento a dos, esta conversión es conceptual y no hay cambios en el patrón de bits (si no hay truncamiento). ]

EDIT2: niveles de advertencia de MSVC

Lo que se advierte sobre los diferentes niveles de advertencia de MSVC son, por supuesto, las decisiones tomadas por los desarrolladores. Como lo veo, sus elecciones en relación con la igualdad firmado / no firmado frente a comparaciones mayores / menores tienen sentido, esto es completamente subjetivo, por supuesto:

-1 == -1significa lo mismo que -1 == (unsigned) -1- Encuentro que es un resultado intuitivo.

-1 < 2 no significa lo mismo que -1 < (unsigned) 2- Esto es menos intuitivo a primera vista, y la OMI merece una advertencia "antes".

Erik
fuente
¿Cómo se puede convertir firmado a no firmado? ¿Cuál es la versión sin firmar del valor firmado -1? (con signo -1 = 1111, mientras que sin signo 15 = 1111, bit a bit pueden ser iguales, pero no son lógicamente iguales). Entiendo que si fuerza esta conversión, funcionará, pero ¿por qué haría eso el compilador? Es ilógico. Además, como comenté anteriormente, cuando aparecí las advertencias, apareció la advertencia == faltante, que parece respaldar lo que digo.
Peter
1
Como dice 4.7 / 2, firmado a unsigned significa que no hay cambios en el patrón de bits para el complemento a dos. En cuanto a por qué el compilador hace esto, es requerido por el estándar C ++. Creo que el razonamiento detrás de las advertencias de VS en diferentes niveles es la posibilidad de que una expresión no sea intencionada, y estoy de acuerdo con ellos en que la comparación de igualdad de firmado / no firmado es "menos probable" que sea un problema que las comparaciones de desigualdad. Por supuesto, esto es subjetivo: son elecciones realizadas por los desarrolladores del compilador VC.
Erik
Ok, creo que casi lo entiendo. La forma en que leo eso es que el compilador está (conceptualmente) haciendo: 'if (((unsigned _int64) 0x7fffffff) == ((unsigned _int64) 0xffffffff))', porque _int64 es el tipo más pequeño que puede representar tanto 0x7fffffff como 0xffffffff en términos sin firmar?
Peter
2
En realidad, comparar (unsigned)-1o -1ues a menudo peor que comparar -1. Eso es porque (unsigned __int64)-1 == -1, pero (unsigned __int64)-1 != (unsigned)-1. Entonces, si el compilador da una advertencia, intentas silenciarlo convirtiéndolo en unsigned o usando -1uy si el valor realmente es de 64 bits o lo cambias a uno más tarde, ¡romperás tu código! Y recuerde que size_tno está firmado, es de 64 bits solo en plataformas de 64 bits y el uso de -1 para un valor no válido es muy común.
Jan Hudec
1
Quizás los cpmpilers no deberían hacer eso entonces. Si compara firmado y no firmado, simplemente verifique si el valor firmado es negativo. Si es así, se garantiza que será menor que el sin firmar independientemente.
CashCow
32

Por qué las advertencias firmadas / no firmadas son importantes y los programadores deben prestarles atención, se demuestra en el siguiente ejemplo.

¿Adivina la salida de este código?

#include <iostream>

int main() {
        int i = -1;
        unsigned int j = 1;
        if ( i < j ) 
            std::cout << " i is less than j";
        else
            std::cout << " i is greater than j";

        return 0;
}

Salida:

i is greater than j

¿Sorprendido? Demostración en línea: http://www.ideone.com/5iCxY

En resumen: en comparación, si un operando es unsigned, entonces el otro operando se convierte implícitamente en unsigned si su tipo está firmado.

Nawaz
fuente
2
¡El tiene razón! Es tonto, pero tiene razón. Este es un problema importante con el que nunca me había encontrado antes. ¿Por qué no convierte el valor sin firmar en un valor con signo (más grande)? Si lo hace "if (i <((int) j))" funciona como es de esperar. Aunque "if (i <((_int64) j))" tendría más sentido (asumiendo, lo cual no se puede, que _int64 es el doble del tamaño de int).
Peter
6
@Peter "¿Por qué no convierte el desamparado en un valor con signo (mayor)? La respuesta es simple: puede que no haya un valor con signo más grande. En una máquina de 32 bits, en los días no mucho, tanto int como long eran 32 bits, y no había nada más grande. Al comparar firmado y no firmado, los primeros compiladores de C ++ convirtieron ambos a firmado. Por no olvidar las razones, el comité de estándares C cambió esto. Su mejor solución es evitar la falta de firma tanto como sea posible.
James Kanze
5
@JamesKanze: sospecho que también tiene que ver con el hecho de que el resultado del desbordamiento firmado es un comportamiento indefinido, mientras que el resultado del desbordamiento sin firmar no lo es y, por lo tanto, la conversión de valor con signo negativo a sin signo se define mientras que la conversión de valor sin signo grande a con signo negativo el valor no lo es .
Jan Hudec
2
@James El compilador siempre podría generar ensamblado que implementaría la semántica más intuitiva de esta comparación sin convertir a un tipo más grande. En este ejemplo en particular, sería suficiente comprobar primero si i<0. Entonces ies más pequeño que jseguro. Si ino es menor que cero, entonces ìse puede convertir con seguridad a unsigned para compararlo j. Claro, las comparaciones entre firmados y no firmados serían más lentas, pero su resultado sería más correcto en cierto sentido.
Sven
@Sven estoy de acuerdo. El estándar podría haber requerido que las comparaciones funcionen para todos los valores reales, en lugar de convertir a uno de los dos tipos. Sin embargo, esto solo funcionaría para las comparaciones; Sospecho que el comité no quería reglas diferentes para las comparaciones y otras operaciones (y no quería atacar el problema de especificar comparaciones cuando el tipo que realmente se compara no existía).
James Kanze
4

El operador == solo hace una comparación bit a bit (por división simple para ver si es 0).

Las comparaciones de menor / mayor que dependen mucho más del signo del número.

Ejemplo de 4 bits:

1111 = 15? o -1?

así que si tienes 1111 <0001 ... es ambiguo ...

pero si tienes 1111 == 1111 ... Es lo mismo aunque no quisiste que fuera.

Yochai Timmer
fuente
Entiendo esto, pero no responde a mi pregunta. Como señala, 1111! = 1111 si los signos no coinciden. El compilador sabe que hay un desajuste de los tipos, entonces, ¿por qué no advierte sobre ello? (Mi punto es que mi código podría contener muchos desajustes de los que no se me advierte)
Peter
Es la forma en que está diseñado. La prueba de igualdad verifica la similitud. Y es similar. Estoy de acuerdo contigo en que no debería ser así. ¡Podrías hacer una macro o algo que sobrecargue x == y para ser! ((X <y) || (x> y))
Yochai Timmer
1

En un sistema que representa los valores usando 2 complementos (la mayoría de los procesadores modernos) son iguales incluso en su forma binaria. Esta puede ser la razón por la que el compilador no se queja de a == b .

Y para mí, es extraño que el compilador no te advierte sobre a == ((int) b) . Creo que debería darte una advertencia de truncamiento de enteros o algo así.

Hossein
fuente
1
La filosofía de C / C ++ es: el compilador confía en que el desarrollador sabe lo que está haciendo al convertir explícitamente entre tipos. Por lo tanto, no hay advertencia (al menos de forma predeterminada, creo que hay compiladores que generan advertencias para esto si el nivel de advertencia se establece más alto que el predeterminado).
Péter Török
0

La línea de código en cuestión no genera una advertencia C4018 porque Microsoft ha utilizado un número de advertencia diferente (es decir, C4389 ) para manejar ese caso, y C4389 no está habilitado de forma predeterminada (es decir, en el nivel 3).

De los documentos de Microsoft para C4389:

// C4389.cpp
// compile with: /W4
#pragma warning(default: 4389)

int main()
{
   int a = 9;
   unsigned int b = 10;
   if (a == b)   // C4389
      return 0;
   else
      return 0;
};

Las otras respuestas han explicado bastante bien por qué Microsoft podría haber decidido hacer un caso especial del operador de igualdad, pero encuentro que esas respuestas no son muy útiles sin mencionar C4389 o cómo habilitarlo en Visual Studio .

También debo mencionar que si va a habilitar C4389, también podría considerar habilitar C4388. Desafortunadamente, no hay documentación oficial para C4388, pero parece aparecer en expresiones como las siguientes:

int a = 9;
unsigned int b = 10;
bool equal = (a == b); // C4388
Tim Rae
fuente