c++ - significa - ¿Es más probable que el uso de un int sin signo en lugar de un int firmado tenga errores? ¿Por qué?
firma por poder ejemplo (6)
En la Guía de estilo de Google C ++ , sobre el tema "Enteros sin signo", se sugiere que
Debido a un accidente histórico, el estándar de C ++ también usa enteros sin signo para representar el tamaño de los contenedores. Muchos miembros del organismo de estándares creen que esto es un error, pero en este momento es imposible corregirlo. El hecho de que la aritmética no firmada no modele el comportamiento de un entero simple, sino que está definido por el estándar para modelar aritmética modular (envolver alrededor de desbordamiento / subdesbordamiento), significa que el compilador no puede diagnosticar una clase significativa de errores.
¿Qué está mal con la aritmética modular? ¿No es ese el comportamiento esperado de un int sin firmar?
¿A qué tipo de errores (una clase significativa) se refiere la guía? Bichos desbordados?
No utilice un tipo sin signo simplemente para afirmar que una variable no es negativa.
Una de las razones por las que puedo pensar en usar int firmado sobre int sin firmar es que si se desborda (a negativo), es más fácil de detectar.
¿Por qué es más probable que el uso de un int sin signo cause errores que el uso de un int firmado?
El uso de un tipo sin firma no es más probable que cause errores que el uso de un tipo firmado con ciertas clases de tareas.
Utilice la herramienta adecuada para el trabajo.
¿Qué está mal con la aritmética modular? ¿No es ese el comportamiento esperado de un int sin firmar?
¿Por qué es más probable que el uso de un int sin signo cause errores que el uso de un int firmado?
Si la tarea es bien emparejada: nada mal. No, no es más probable.
El algoritmo de seguridad, cifrado y autenticación cuenta con matemática modular sin firma.
Los algoritmos de compresión / descompresión, así como varios formatos gráficos, se benefician y tienen menos problemas con las matemáticas sin firmar .
En cualquier momento en que se utilicen operadores y turnos de bits, las operaciones no firmadas no se ensucian con los problemas de extensión de signo de matemática firmada .
La matemática de enteros con signo tiene una apariencia intuitiva y se siente fácilmente comprendida por todos los estudiantes, incluida la codificación. C / C ++ no estaba dirigido originalmente ni ahora debería ser un intro-lenguaje. Para la codificación rápida que emplea redes de seguridad relacionadas con el desbordamiento, otros idiomas son más adecuados. Para el código Lean Fast, C asume que los programadores saben lo que están haciendo (tienen experiencia).
Un error de las matemáticas
firmadas
hoy en día es la ubicua
int
32 bits que con tantos problemas es lo suficientemente amplia para las tareas comunes sin verificar el rango.
Esto lleva a la complacencia contra la que no se codifica el desbordamiento.
En cambio,
for (int i=0; i < n; i++)
int len = strlen(s);
se ve como OK porque
n
se supone <
INT_MAX
y las cadenas nunca serán demasiado largas, en lugar de estar protegidas a rango completo en el primer caso o usar
size_t
,
unsigned
o incluso
long long
en el segundo.
C / C ++ se desarrolló en una era que incluía 16 bits así como 32 bits
int
y el bit adicional que ofrece un tamaño sin signo de 16 bits era significativo.
Se necesitaba atención con respecto a los problemas de desbordamiento, ya sea
int
o
unsigned
.
Con las aplicaciones de 32 bits (o más amplias) de Google en plataformas
int/unsigned
16 bits, se presta poca atención al desbordamiento +/- de
int
dado su amplio rango.
Esto tiene sentido para tales aplicaciones para alentar a
int
sobre
unsigned
.
Sin embargo,
int
math no está bien protegido.
Las preocupaciones estrechas de 16 bits
int/unsigned
se aplican hoy en día con aplicaciones integradas seleccionadas.
Las pautas de Google se aplican bien al código que escriben hoy. No es una guía definitiva para el rango más amplio de código C / C ++.
Una de las razones por las que puedo pensar en usar int firmado sobre int sin firmar es que si se desborda (a negativo), es más fácil de detectar.
En C / C ++, el desbordamiento de matemática int firmado es un comportamiento indefinido y, por lo tanto, no es más fácil de detectar que el comportamiento definido de las matemáticas no firmadas .
Como comentó @Chris Uzdavinis , la mezcla con y sin firma se evita mejor por todos (especialmente los principiantes) y, por lo demás, se codifica cuidadosamente cuando sea necesario.
Algunas de las respuestas aquí mencionan las sorprendentes reglas de promoción entre los valores firmados y no firmados, pero esto parece más bien un problema relacionado con la mezcla de valores firmados y no firmados, y no necesariamente explica por qué se prefiere la firma sobre la no firmada , fuera de los escenarios de mezcla.
En mi experiencia, aparte de las comparaciones y las reglas de promoción mixtas, hay dos razones principales por las que los valores sin firma son imanes de error grandes.
Los valores sin firmar tienen una discontinuidad en cero, el valor más común en la programación
Los enteros sin signo y con signo tienen una
discontinuidad
en sus valores mínimo y máximo, donde se envuelven (sin signo) o causan un comportamiento indefinido (con signo).
Para los que están
unsigned
estos puntos están en
cero
y en
UINT_MAX
.
Para
int
están en
INT_MIN
y
INT_MAX
.
Los valores típicos de
INT_MIN
e
INT_MAX
en el sistema con valores
int
4 bytes son
-2^31
y
2^31-1
, y en tal sistema,
UINT_MAX
suele ser
2^32-1
.
El problema principal que induce errores en
unsigned
que no se aplica a
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 resta algo de un valor
unsigned
y resulta que es cero, acaba de obtener 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 los últimos 0.5 :
for (size_t i = 0; i < v.size() - 1; i++) { // do something }
Esto funciona bien hasta que un día pasas 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.
Necesitas escribirlo así:
for (size_t i = 0; i + 1 < v.size(); i++) { // do something }
Por lo tanto, se puede "arreglar" en este caso, pero solo pensando cuidadosamente en la naturaleza no firmada 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 tanto, el "lado" de la comparación que debe colocar depende de la firmeza. - Ahora el código se vuelve
muy
desordenado.
Hay un problema similar con el código que intenta iterar hasta e incluyendo cero.
Algo parecido a
while (index-- > 0)
funciona bien, pero el equivalente aparentemente equivalente a
while (--index >= 0)
nunca terminará por un valor sin signo.
Su compilador puede advertirle cuando el lado derecho es cero
literal
, pero ciertamente no si es un valor determinado en tiempo de ejecución.
Contrapunto
Algunos podrían argumentar que los valores firmados también tienen dos discontinuidades, así que ¿por qué elegir sin firmar? La diferencia es que ambas discontinuidades están muy lejos de cero (máximo). Realmente considero que este es un problema separado de "desbordamiento", tanto los valores firmados como los no firmados pueden desbordarse a valores muy grandes. En muchos casos, el desbordamiento es imposible debido a las restricciones en el posible rango de los 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 es a menudo minúscula en comparación con un error "en cero", y el desbordamiento se produce también para valores sin firma . Así que unsigned combina lo peor de ambos mundos: potencialmente desbordante con valores de magnitud muy grandes y una discontinuidad en cero. Firmado solo tiene el primero.
Muchos argumentarán "usted pierde un poco" con unsigned. Esto es a menudo cierto, pero no siempre (si necesita representar diferencias entre valores sin signo, 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 rara donde diga 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 no firmados te compran un poco: no te compran mucho: si tuvieras que apoyar más de 2 mil millones de "cosas", es probable que pronto tengas que soportar más de 4 mil millones.
Lógicamente, los valores sin firmar son un subconjunto de valores con signo
Matemáticamente, los valores sin signo (enteros no negativos) son un subconjunto de enteros con signo (llamados simplemente _integers). 2 . Sin embargo, los valores firmados naturalmente se eliminan de las operaciones únicamente en valores sin signo , como la resta. Podríamos decir que los valores sin firmar no se cierran bajo la resta. Lo mismo no es cierto de los valores firmados.
¿Desea encontrar el "delta" entre dos índices sin firmar en un archivo? Bueno, es 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. Cuando se trata de valores no firmados como números, a menudo encontrará que los valores firmados (lógicamente) siguen apareciendo de todos modos, por lo que también puede comenzar con firmados.
Contrapunto
Como se mencionó 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 posibilidad de restar, 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 puede representar la mitad. 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 aleatoriamente 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-bit cierra la puerta en muchos de los motivos para usar valores firmados como números
Creo que los argumentos anteriores ya eran convincentes para los valores de 32 bits, pero los casos de desbordamiento, que afectan tanto a los firmados como a los no firmados en diferentes umbrales, ocurren para valores de 32 bits, ya que "2 mil millones" es un número que puede ser superado por muchos Cantidades abstractas y físicas (miles de millones de dólares, billones de nanosegundos, arreglos con billones de elementos). Entonces, si alguien está lo suficientemente convencido por la duplicación del rango positivo para los valores sin firmar, puede hacer que el desbordamiento sea importante y que favorezca ligeramente a los no firmados.
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 quintillones . Son muchos nanosegundos (alrededor de 292 años de valor) y mucho dinero. También es una matriz más grande que cualquier computadora que tenga RAM en un espacio de direcciones coherente durante mucho tiempo. Entonces, ¿tal vez 9 quintillones son suficientes para todos (por ahora)?
Cuándo usar valores sin firmar
Tenga en cuenta que la guía de estilo no prohíbe ni necesariamente desalienta el uso de números no firmados. Concluye con:
No utilice un tipo sin signo simplemente para afirmar que una variable no es negativa.
De hecho, hay buenos usos para las variables sin signo:
-
Cuando desea tratar una cantidad de N bits no es un número entero, sino simplemente 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
yuint64_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 solo se opera con los operadores a nivel de bits como~
,|
,&
,^
,>>
y así sucesivamente, y no con las operaciones aritméticas como+
,-
,*
,/
etc.Sin firma es ideal aquí porque el comportamiento de los operadores bitwise está bien definido y estandarizado. Los valores firmados 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 firmar le dan 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 esto 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 de
size_t
aquí, por lo general 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 dar lugar a valores con signo (lógicamente), pero no hay problema correspondiente con valores con signo (ya que los valores con signo ya incluyen valores sin signo).
Como se indicó, la mezcla
unsigned
y
signed
podría llevar a un comportamiento inesperado (incluso si está bien definido).
Suponga que desea iterar sobre todos los elementos de vector, excepto los últimos cinco, podría escribir erróneamente:
for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct
Supongamos que
v.size() < 5
, entonces, como
v.size()
unsigned
está
unsigned
,
s.size() - 5
sería un número muy grande, y entonces
i < v.size() - 5
sería
true
para un rango de valor más esperado de
i
.
Y UB luego sucede rápidamente (fuera del acceso enlazado una vez que
i >= v.size()
)
Si
v.size()
hubiera devuelto un valor firmado, entonces
s.size() - 5
habría sido negativo, y en el caso anterior, la condición sería falsa de inmediato.
Por otro lado, el índice debe estar entre
[0; v.size()[
[0; v.size()[
unsigned
tiene sentido.
Firmado también tiene su propio problema como UB con desbordamiento o comportamiento definido por la implementación para el desplazamiento correcto de un número con signo negativo, pero una fuente menos frecuente de error para la iteración.
Tengo algo de experiencia con la guía de estilo de Google, también conocida como la Guía del autostopista para directivas locas de programadores erróneos que ingresaron a la compañía hace mucho tiempo. Esta guía en particular es solo un ejemplo de las docenas de reglas chifladas en ese libro.
Los errores solo se producen con tipos sin signo si intenta hacer aritmética con ellos (consulte el ejemplo de Chris Uzdavinis más arriba), en otras palabras, si los usa como números. Los tipos no firmados no están diseñados para almacenar cantidades numéricas, están diseñados para almacenar recuentos como el tamaño de los contenedores, que nunca pueden ser negativos, y pueden y deben usarse para ese propósito.
La idea de usar tipos aritméticos (como enteros con signo) para almacenar tamaños de contenedores es estúpida. ¿Usarías un doble para almacenar el tamaño de una lista, también? Que hay personas en Google que almacenan tamaños de contenedores utilizando tipos aritméticos y que requieren que otros hagan lo mismo, dice algo acerca de la compañía. Una cosa que noté acerca de tales dictados es que cuanto más tontos son, más necesitan ser reglas estrictas de hacer o de despedir, porque de lo contrario las personas con sentido común ignorarían la regla.
Uno de los ejemplos más espeluznantes de un error es cuando MEZCLA valores firmados y sin firmar:
#include <iostream>
int main() {
auto qualifier = -1 < 1u ? "makes" : "does not make";
std::cout << "The world " << qualifier << " sense" << std::endl;
}
La salida:
El mundo no tiene sentido.
A menos que tenga una aplicación trivial, es inevitable que termine con combinaciones peligrosas entre valores firmados y no firmados (que resulten en errores de tiempo de ejecución) o si genera advertencias y las convierte en errores de tiempo de compilación, termina con una gran cantidad de static_casts en tu código. Es por eso que es mejor usar estrictamente enteros con signo para tipos para matemáticas o comparación lógica. Utilice solo sin signo para máscaras de bits y tipos que representan bits.
Modelar un tipo para que no se firme según el dominio esperado de los valores de sus números es una mala idea. La mayoría de los números están más cerca de 0 que de 2 mil millones, por lo que con los tipos sin firma, muchos de sus valores están más cerca del límite del rango válido. Para empeorar las cosas, el valor final puede estar en un rango positivo conocido, pero al evaluar las expresiones, los valores intermedios pueden desbordarse y, si se usan en forma intermedia, pueden ser valores MUY incorrectos. Finalmente, incluso si se espera que sus valores sean siempre positivos, eso no significa que no interactúen con otras variables que pueden ser negativas, por lo que terminará con una situación forzada de mezcla de tipos con y sin signo, que es El peor lugar para estar.
Usando tipos sin signo para representar valores no negativos ...
- es más probable que cause errores que involucren la promoción de tipos, cuando se usan valores con y sin signo, como lo demuestran otras respuestas y se analizan en profundidad, pero
- es menos probable que cause errores que involucren la elección de tipos con dominios capaces de representar valores no deseables o que no se pueden aceptar. En algunos lugares, asumirá que el valor está en el dominio y puede tener un comportamiento inesperado y potencialmente peligroso cuando otro valor se infiltre de alguna manera.
Las Pautas de codificación de Google ponen énfasis en el primer tipo de consideración. Otros conjuntos de lineamientos, como los Lineamientos Básicos de C ++ , ponen más énfasis en el segundo punto. Por ejemplo, considere la Guía I.12 :
I.12: Declare un puntero que no debe ser nulo como
not_null
Razón
Para ayudar a evitar la desreferenciación de errores nullptr. Para mejorar el rendimiento evitando verificaciones redundantes de
nullptr
.Ejemplo
int length(const char* p); // it is not clear whether length(nullptr) is valid length(nullptr); // OK? int length(not_null<const char*> p); // better: we can assume that p cannot be nullptr int length(const char* p); // we must assume that p can be nullptr
Al indicar la intención en la fuente, los implementadores y las herramientas pueden proporcionar mejores diagnósticos, como encontrar algunas clases de errores a través del análisis estático, y realizar optimizaciones, como eliminar sucursales y pruebas nulas.
Por supuesto, podría
non_negative
por un envoltorio
non_negative
para enteros, que evita ambas categorías de errores, pero que tendría sus propios problemas ...