Algunas de las respuestas aquí mencionar las reglas de la promoción sorprendentes entre los valores con y sin signo, pero que parece más como un problema relacionado con la mezcla de los valores con y sin signo, y no necesariamente explica por qué firmados serían preferibles variables a lo largo sin signo exterior de escenarios de mezcla.
En mi experiencia, fuera de las comparaciones mixtas y las reglas de promoción, hay dos razones principales por las que los valores sin firmar son imanes de errores de la siguiente manera.
Los valores sin signo tienen una discontinuidad en cero, el valor más común en programación.
Tanto los enteros sin signo como con signo tienen discontinuidades en sus valores mínimo y máximo, donde se envuelven (sin signo) o causan un comportamiento indefinido (con signo). Porque unsigned
estos puntos están en cero y UINT_MAX
. Porque int
están en INT_MIN
y INT_MAX
. Los valores típicos de INT_MIN
y INT_MAX
en el sistema con int
valores de 4 bytes son -2^31
y 2^31-1
, y en tal sistema UINT_MAX
es típicamente 2^32-1
.
El problema principal que induce errores con unsigned
eso no se aplica int
es que tiene una discontinuidad en cero . Cero, por supuesto, es un valor muy común en los programas, junto con otros valores pequeños como 1,2,3. Es común sumar y restar valores pequeños, especialmente 1, en varias construcciones, y si restas algo de un unsigned
valor y resulta ser cero, obtienes un valor positivo masivo y un error casi seguro.
Considere que el código itera sobre todos los valores en un vector por índice, excepto el último 0.5 :
for (size_t i = 0; i < v.size() - 1; i++) {
Esto funciona bien hasta que un día pasa en un vector vacío. En lugar de hacer cero iteraciones, obtienes v.size() - 1 == a giant number
1 y harás 4 mil millones de iteraciones y casi tendrás una vulnerabilidad de desbordamiento de búfer.
Tienes que escribirlo así:
for (size_t i = 0; i + 1 < v.size(); i++) {
Por lo tanto, se puede "arreglar" en este caso, pero solo si se piensa detenidamente en la naturaleza sin firmar de size_t
. A veces no puede aplicar la corrección anterior porque, en lugar de una constante, tiene un desplazamiento variable que desea aplicar, que puede ser positivo o negativo: por lo que el "lado" de la comparación en el que debe colocarlo depende del signo - ahora el código se vuelve realmente complicado.
Existe un problema similar con el código que intenta iterar hasta cero, inclusive. Algo como while (index-- > 0)
funciona bien, pero el aparentemente equivalente while (--index >= 0)
nunca terminará por un valor sin firmar. Su compilador puede advertirle cuando el lado derecho es literal cero, pero ciertamente no si es un valor determinado en tiempo de ejecución.
Contrapunto
Algunos podrían argumentar que los valores con signo también tienen dos discontinuidades, entonces, ¿por qué elegir sin firmar? La diferencia es que ambas discontinuidades están muy (como máximo) lejos de cero. Realmente considero que esto es un problema separado de "desbordamiento", tanto los valores firmados como los no firmados pueden desbordarse en valores muy grandes. En muchos casos, el desbordamiento es imposible debido a las limitaciones del posible rango de valores, y el desbordamiento de muchos valores de 64 bits puede ser físicamente imposible). Incluso si es posible, la posibilidad de un error relacionado con el desbordamiento suele ser minúscula en comparación con un error "en cero", y el desbordamiento también se produce para los valores sin firmar . So unsigned combina lo peor de ambos mundos: desbordamiento potencial con valores de magnitud muy grandes y una discontinuidad en cero. Firmado solo tiene el primero.
Muchos dirán que "pierdes un poco" con unsigned. Esto a menudo es cierto, pero no siempre (si necesita representar diferencias entre valores sin firmar, perderá ese bit de todos modos: muchas cosas de 32 bits están limitadas a 2 GiB de todos modos, o tendrá un área gris extraña donde digamos un archivo puede tener 4 GiB, pero no puede usar ciertas API en la segunda mitad de 2 GiB).
Incluso en los casos en los que unsigned te compra un poco: no te compra mucho: si tuvieras que soportar más de 2 mil millones de "cosas", probablemente pronto tendrás que soportar más de 4 mil millones.
Lógicamente, los valores sin signo son un subconjunto de valores con signo
Matemáticamente, los valores sin signo (enteros no negativos) son un subconjunto de enteros con signo (simplemente llamados _ enteros). 2 . Sin embargo, los valores con signo emergen naturalmente de las operaciones únicamente en valores sin signo , como la resta. Podríamos decir que los valores sin firmar no se cierran mediante sustracción. No ocurre lo mismo con los valores con signo.
¿Quiere encontrar el "delta" entre dos índices sin firmar en un archivo? Bueno, será mejor que hagas la resta en el orden correcto, o de lo contrario obtendrás la respuesta incorrecta. Por supuesto, a menudo necesita una verificación de tiempo de ejecución para determinar el orden correcto. Al tratar con valores sin signo como números, a menudo encontrará que los valores con signo (lógicamente) siguen apareciendo de todos modos, por lo que también puede comenzar con firmado.
Contrapunto
Como se menciona en la nota al pie (2) anterior, los valores con signo en C ++ no son en realidad un subconjunto de valores sin signo del mismo tamaño, por lo que los valores sin signo pueden representar el mismo número de resultados que los valores con signo.
Es cierto, pero el rango es menos útil. Considere la resta y los números sin signo con un rango de 0 a 2N, y los números con signo con un rango de -N a N. Las restas arbitrarias dan como resultado resultados en el rango de -2N a 2N en ambos casos, y cualquier tipo de entero solo puede representar la mitad. Bueno, resulta que la región centrada alrededor de cero de -N a N suele ser mucho más útil (contiene más resultados reales en el código del mundo real) que el rango de 0 a 2N. Considere cualquier distribución típica que no sea uniforme (log, zipfian, normal, lo que sea) y considere restar valores seleccionados al azar de esa distribución: muchos más valores terminan en [-N, N] que [0, 2N] (de hecho, la distribución resultante siempre está centrado en cero).
64 bits cierra la puerta a muchas de las razones para usar valores con signo como números
Creo que los argumentos anteriormente ya fueron convincentes para los valores de 32 bits, pero los casos de desbordamiento, que afectan tanto con y sin signo en diferentes umbrales, no se produce para valores de 32 bits, ya que "2 mil millones" es un número que puede superado por muchos cantidades abstractas y físicas (miles de millones de dólares, miles de millones de nanosegundos, matrices con miles de millones de elementos). Entonces, si alguien está lo suficientemente convencido por la duplicación del rango positivo para valores sin firmar, puede argumentar que el desbordamiento sí importa y favorece ligeramente a unsigned.
Fuera de los dominios especializados, los valores de 64 bits eliminan en gran medida esta preocupación. Los valores de 64 bits firmados tienen un rango superior de 9.223.372.036.854.775.807, más de nueve trillones . Eso es muchos nanosegundos (unos 292 años) y mucho dinero. También es una matriz más grande de lo que es probable que cualquier computadora tenga RAM en un espacio de direcciones coherente durante mucho tiempo. Entonces, ¿quizás 9 trillones es suficiente para todos (por ahora)?
Cuando usar valores sin firmar
Tenga en cuenta que la guía de estilo no prohíbe ni desalienta necesariamente el uso de números sin firmar. Concluye con:
No utilice un tipo sin firmar simplemente para afirmar que una variable no es negativa.
De hecho, existen buenos usos para las variables sin firmar:
Cuando desee tratar una cantidad de N bits no como un número entero, sino simplemente como una "bolsa de bits". Por ejemplo, como una máscara de bits o un mapa de bits, o N valores booleanos o lo que sea. Este uso a menudo va de la mano con los tipos de ancho fijo como uint32_t
y uint64_t
ya que a menudo desea saber el tamaño exacto de la variable. Un indicio de que una variable en particular merece este tratamiento es que sólo se opera en él con los bit a bit operadores como ~
, |
, &
, ^
, >>
y así sucesivamente, y no con las operaciones aritméticas tales como +
, -
, *
, /
etc.
Unsigned es ideal aquí porque el comportamiento de los operadores bit a bit está bien definido y estandarizado. Los valores con signo tienen varios problemas, como un comportamiento indefinido y no especificado al cambiar, y una representación no especificada.
Cuando realmente quieres aritmética modular. A veces, realmente quieres aritmética modular 2 ^ N. En estos casos, el "desbordamiento" es una característica, no un error. Los valores sin signo le brindan lo que desea aquí, ya que están definidos para usar aritmética modular. Los valores firmados no se pueden usar (fácil y eficientemente) en absoluto, ya que tienen una representación no especificada y el desbordamiento no está definido.
0.5 Después de escribir esto, me di cuenta de que es casi idéntico al ejemplo de Jarod , que no había visto, y por una buena razón, ¡es un buen ejemplo!
1 Estamos hablando size_t
aquí, por lo que generalmente es 2 ^ 32-1 en un sistema de 32 bits o 2 ^ 64-1 en uno de 64 bits.
2 En C ++ este no es exactamente el caso porque los valores sin signo contienen más valores en el extremo superior que el tipo con signo correspondiente, pero existe el problema básico de que la manipulación de valores sin signo puede resultar en valores con signo (lógicamente), pero no hay un problema correspondiente con valores firmados (dado que los valores firmados ya incluyen valores sin firmar).
unsigned int x = 0; --x;
y ver qué sex
convierte. Sin controles de límite, el tamaño podría obtener repentinamente un valor inesperado que podría conducir fácilmente a UB.