¿Se define el comportamiento de la resta de enteros sin signo?

100

Me encontré con el código de alguien que parece creer que hay un problema al restar un entero sin signo de otro entero del mismo tipo cuando el resultado sería negativo. Entonces, ese código como este sería incorrecto incluso si funciona en la mayoría de las arquitecturas.

unsigned int To, Tf;

To = getcounter();
while (1) {
    Tf = getcounter();
    if ((Tf-To) >= TIME_LIMIT) {
        break;
    } 
}

Esta es la única cita vagamente relevante del estándar C que pude encontrar.

Un cálculo que involucre operandos sin signo nunca puede desbordarse, porque un resultado que no puede ser representado por el tipo entero sin signo resultante se reduce módulo al número que es uno mayor que el valor más grande que puede ser representado por el tipo resultante.

Supongo que se podría tomar esa cita en el sentido de que cuando el operando derecho es más grande, la operación se ajusta para que sea significativa en el contexto de números truncados de módulo.

es decir

0x0000 - 0x0001 == 0x 1 0000 - 0x0001 == 0xFFFF

en lugar de utilizar la semántica firmada dependiente de la implementación:

0x0000 - 0x0001 == (sin firmar) (0 + -1) == (0xFFFF pero también 0xFFFE o 0x8001)

¿Cuál o qué interpretación es la correcta? ¿Está definido en absoluto?

LihO
fuente
3
La elección de palabras en el estándar es lamentable. Que “nunca se pueda desbordar” significa que no es una situación de error. Usando la terminología en el estándar, en lugar de desbordar el valor "envuelve".
danorton

Respuestas:

107

El resultado de una resta que genera un número negativo en un tipo sin signo está bien definido:

  1. [...] Un cálculo que involucra operandos sin signo nunca puede desbordarse, porque un resultado que no puede ser representado por el tipo entero sin signo resultante se reduce módulo al número que es uno mayor que el valor más grande que puede ser representado por el tipo resultante. (ISO / IEC 9899: 1999 (E) §6.2.5 / 9)

Como puede ver, (unsigned)0 - (unsigned)1es igual a -1 módulo UINT_MAX + 1, o en otras palabras, UINT_MAX.

Tenga en cuenta que, aunque dice "Un cálculo que implica operandos sin firmar nunca puede desbordarse", lo que podría llevarlo a creer que solo se aplica para exceder el límite superior, esto se presenta como una motivación para la parte vinculante real de la oración: "a el resultado que no puede ser representado por el tipo entero sin signo resultante se reduce módulo el número que es uno mayor que el valor más grande que puede ser representado por el tipo resultante. " Esta frase no se limita al desbordamiento del límite superior del tipo y se aplica igualmente a valores demasiado bajos para ser representados.

bdonlan
fuente
2
¡Gracias! Ahora veo la interpretación que me faltaba. Sin embargo, creo que podrían haber elegido una redacción más clara.
4
Me siento mucho mejor ahora, sabiendo que si cualquier tirada de adición sin firmar alrededor a cero y causa caos, será porque uintsiempre tenía la intención de representar a la matemática del anillo de los enteros 0a través UINT_MAX, con las operaciones de suma y módulo de multiplicación UINT_MAX+1, y no porque de un desbordamiento. Sin embargo, plantea la pregunta de por qué, si los anillos son un tipo de datos tan fundamental, el lenguaje no ofrece un soporte más general para anillos de otros tamaños.
Theodore Murdock
2
@TheodoreMurdock Creo que la respuesta a esa pregunta es simple. Por lo que puedo decir, el hecho de que sea un anillo es una consecuencia, no una causa. El requisito real es que los tipos sin firmar deben tener todos sus bits participando en la representación del valor. El comportamiento similar a un anillo fluye naturalmente de eso. Si desea tal comportamiento de otros tipos, haga su aritmética y luego aplique el módulo requerido; que utiliza operadores fundamentales.
underscore_d
@underscore_d Por supuesto ... está claro por qué tomaron la decisión de diseño. Es divertido que escribieran la especificación más o menos como "no hay desbordamiento / subdesbordamiento aritmético porque el tipo de datos se especifica como un anillo", como si esta elección de diseño significara que los programadores no tuvieran que evitar cuidadosamente los excesos y los insuficientes -fluyen o sus programas fallan espectacularmente.
Theodore Murdock
120

Cuando trabaja con tipos sin firmar , se lleva a cabo la aritmética modular (también conocida como comportamiento "envolvente" ). Para comprender esta aritmética modular , solo eche un vistazo a estos relojes:

ingrese la descripción de la imagen aquí

9 + 4 = 1 ( 13 mod 12 ), entonces en la otra dirección es: 1 - 4 = 9 ( -3 mod 12 ). El mismo principio se aplica al trabajar con tipos sin firmar. Si el tipo de resultado es unsigned, entonces se lleva a cabo la aritmética modular.


Ahora observe las siguientes operaciones que almacenan el resultado como un unsigned int:

unsigned int five = 5, seven = 7;
unsigned int a = five - seven;      // a = (-2 % 2^32) = 4294967294 

int one = 1, six = 6;
unsigned int b = one - six;         // b = (-5 % 2^32) = 4294967291

Cuando desee asegurarse de que el resultado sea signed, guárdelo en signedvariable o conviértalo en signed. Cuando desee obtener la diferencia entre números y asegurarse de que no se aplicará la aritmética modular, debe considerar usar la abs()función definida en stdlib.h:

int c = five - seven;       // c = -2
int d = abs(five - seven);  // d =  2

Tenga mucho cuidado, especialmente al escribir las condiciones, porque:

if (abs(five - seven) < seven)  // = if (2 < 7)
    // ...

if (five - seven < -1)          // = if (-2 < -1)
    // ...

if (one - six < 1)              // = if (-5 < 1)
    // ...

if ((int)(five - seven) < 1)    // = if (-2 < 1)
    // ...

pero

if (five - seven < 1)   // = if ((unsigned int)-2 < 1) = if (4294967294 < 1)
    // ...

if (one - six < five)   // = if ((unsigned int)-5 < 5) = if (4294967291 < 5)
    // ...
LihO
fuente
4
Bonito con los relojes, aunque la prueba haría que esta sea la respuesta correcta. La premisa de la pregunta ya incluye la afirmación de que todo esto puede ser cierto.
Lightness Races in Orbit
5
@LightnessRacesinOrbit: Gracias. Lo escribí porque creo que alguien podría encontrarlo muy útil. Estoy de acuerdo en que no es una respuesta completa.
LihO
4
La línea int d = abs(five - seven);no es buena. Primero five - sevense calcula: la promoción deja los tipos de operandos como unsigned int, el resultado se calcula en módulo (UINT_MAX+1)y se evalúa como UINT_MAX-1. Entonces este valor es el parámetro real de abs, lo cual es una mala noticia. abs(int)provoca un comportamiento indefinido que pasa el argumento, ya que no está dentro del rango, y abs(long long)probablemente puede contener el valor, pero el comportamiento indefinido ocurre cuando el valor de retorno es forzado inta inicializarse d.
Ben Voigt
1
@LihO: El único operador en C ++ que es sensible al contexto y actúa de manera diferente dependiendo de cómo se use su resultado es un operador de conversión personalizado operator T(). La suma en las dos expresiones que estamos discutiendo se realiza en tipo unsigned int, basado en los tipos de operandos. El resultado de la adición es unsigned int. Luego, ese resultado se convierte implícitamente al tipo requerido en el contexto, una conversión que falla porque el valor no se puede representar en el nuevo tipo.
Ben Voigt
1
@LihO: Puede ayudar pensar en double x = 2/3;vsdouble y = 2.0/3;
Ben Voigt
5

Bueno, la primera interpretación es correcta. Sin embargo, su razonamiento sobre la "semántica firmada" en este contexto es incorrecto.

Nuevamente, su primera interpretación es correcta. La aritmética sin signo sigue las reglas de la aritmética de módulo, lo que significa que se 0x0000 - 0x0001evalúa 0xFFFFpara tipos sin signo de 32 bits.

Sin embargo, la segunda interpretación (la basada en "semántica con signo") también es necesaria para producir el mismo resultado. Es decir, incluso si evalúa 0 - 1en el dominio de tipo firmado y obtiene -1como resultado intermedio, esto -1aún es necesario para producirlo 0xFFFFcuando más tarde se convierta a tipo sin firmar. Incluso si alguna plataforma utiliza una representación exótica para enteros con signo (complemento de 1, magnitud con signo), esta plataforma aún debe aplicar reglas de aritmética de módulo al convertir valores enteros con signo en valores sin signo.

Por ejemplo, esta evaluación

signed int a = 0, b = 1;
unsigned int c = a - b;

todavía está garantizado para producir UINT_MAXen c, incluso si la plataforma está utilizando una representación exótica para enteros con signo.

Hormiga
fuente
4
Creo que te refieres a tipos sin firmar de 16 bits, no a 32 bits.
xioxox
4

Con números sin signo de tipo unsigned into mayor, en ausencia de conversiones de tipo, a-bse define como el resultado del número sin signo que, cuando se agrega b, dará como resultado a. La conversión de un número negativo a sin signo se define como el resultado del número que, cuando se agrega al número original con signo invertido, dará como resultado cero (por lo tanto, convertir -5 a sin signo arrojará un valor que, cuando se suma a 5, dará como resultado cero) .

Tenga en cuenta que los números sin signo más pequeños que unsigned intpueden ser promocionados a tipografía intantes de la resta, el comportamiento de a-bdependerá del tamaño de int.

Super gato
fuente