En la Guía de estilo de Google C ++ , sobre el tema "Enteros sin firmar", se sugiere que
Debido a un accidente histórico, el estándar C ++ también usa números enteros sin firmar para representar el tamaño de los contenedores; muchos miembros del cuerpo de estándares creen que esto es un error, pero es efectivamente imposible de solucionar en este momento. El hecho de que la aritmética sin firmar no modele el comportamiento de un entero simple, sino que esté definida por el estándar para modelar la aritmética modular (envolviendo el overflow / underflow), significa que el compilador no puede diagnosticar una clase significativa de errores.
¿Qué hay de malo en la aritmética modular? ¿No es ese el comportamiento esperado de un int sin firmar?
¿A qué tipo de errores (una clase importante) se refiere la guía? ¿Errores desbordados?
No utilice un tipo sin firmar simplemente para afirmar que una variable no es negativa.
Una razón por la que puedo pensar en usar un int firmado sobre un int no firmado es que si se desborda (a negativo), es más fácil de detectar.
fuente
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.int
desbordamiento y el subdesbordamiento son UB. Es menos probable que experimente una situación en la que unint
trataría de expresar un valor que no puede que una situación que disminuya un valorunsigned int
por debajo de cero, pero el tipo de personas que se sorprenderían con el comportamiento de launsigned int
aritmética es el tipo de personas que también podrían escriba el código que causaría elint
desbordamiento relacionado con UB, como usara < a + 1
para verificar el desbordamiento.Respuestas:
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 yUINT_MAX
. Porqueint
están enINT_MIN
yINT_MAX
. Los valores típicos deINT_MIN
yINT_MAX
en el sistema conint
valores de 4 bytes son-2^31
y2^31-1
, y en tal sistemaUINT_MAX
es típicamente2^32-1
.El problema principal que induce errores con
unsigned
eso no se aplicaint
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 ununsigned
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++) { // do something }
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++) { // do something }
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 equivalentewhile (--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:
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
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 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).
fuente
-ftrapv
esa que pueden capturar todos los desbordamientos firmados, pero no todos los desbordamientos sin firmar. El impacto en el rendimiento no es tan malo, por lo que podría ser razonable realizar la compilación-ftrapv
en algunos escenarios.That's about the age of the universe measured in nanoseconds.
Lo dudo. El universo se trata de lo13.7*10^9 years
viejo que es4.32*10^17 s
o4.32*10^26 ns
. Para representar4.32*10^26
como int necesitas al menos90 bits
.9,223,372,036,854,775,807 ns
solo sería sobre292.5 years
.Como se indicó, la mezcla de
unsigned
ysigned
podría dar lugar a un comportamiento inesperado (incluso si está bien definido).Supongamos que desea iterar sobre todos los elementos del vector excepto los últimos cinco, podría escribir incorrectamente:
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
v.size() < 5
, entonces, que tal comov.size()
estáunsigned
,s.size() - 5
sería un número muy grande, y también loi < v.size() - 5
seríatrue
para un rango de valor más esperado dei
. Y UB luego ocurre rápidamente (fuera de acceso una vezi >= v.size()
)Si
v.size()
hubiera devuelto un valor con signo, entoncess.size() - 5
habría sido negativo y, en el caso anterior, la condición sería falsa inmediatamente.Por otro lado, el índice debe estar entre,
[0; v.size()[
por lo queunsigned
tiene sentido. Signed también tiene su propio problema como UB con desbordamiento o comportamiento definido por la implementación para el desplazamiento a la derecha de un número con signo negativo, pero una fuente de error menos frecuente para la iteración.fuente
i<size()-X
, se debe escribiri+X<size()
. Claro, es algo para recordar, pero no es tan difícil acostumbrarse, en mi opinión.Uno de los ejemplos más espeluznantes de un error es cuando MEZCLAS valores firmados y no firmados:
#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 mezclas peligrosas entre valores firmados y no firmados (lo que resulta en errores de tiempo de ejecución) o si genera advertencias y las comete errores en tiempo de compilación, terminará con una gran cantidad de static_casts en su código. Es por eso que es mejor usar estrictamente enteros con signo para tipos de comparación matemática o lógica. Utilice solo sin firmar para máscaras de bits y tipos que representan bits.
Modelar un tipo para que no esté firmado en función del 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 signo, 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 expresiones, los valores intermedios pueden subdesbordarse y si se usan en forma intermedia pueden ser valores MUY incorrectos. Finalmente, incluso si se espera que sus valores siempre sean positivos, eso no significa que no interactúen con otras variables que pueden ser negativas, por lo que termina con una situación forzada de mezclar tipos con y sin signo, que es el peor lugar para estar.
fuente
No es más probable que el uso de un tipo sin firmar cause errores que el uso de un tipo firmado con ciertas clases de tareas.
Utilice la herramienta adecuada para el trabajo.
Si la tarea está bien adaptada: no hay nada de malo. No, no más probable.
El algoritmo de seguridad, cifrado y autenticación cuenta con matemática modular sin firmar.
Los algoritmos de compresión / descompresión también, así como varios formatos gráficos, se benefician y tienen menos errores con las matemáticas sin firmar .
Cada vez que se utilizan operadores bit a bit y cambios, las operaciones sin firmar no se confunden con los problemas de extensión de signo de las matemáticas con signo .
Las matemáticas enteras con signo tienen un aspecto intuitivo y se sienten fácilmente entendidas por todos, incluidos los estudiantes de codificación. C / C ++ no se apuntó originalmente ni ahora debería ser un lenguaje de introducción. Para la codificación rápida que emplea redes de seguridad en relación con el desbordamiento, otros lenguajes 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 firmado matemáticas hoy en día es el ubicuo de 32 bits
int
que con tantos problemas es también lo suficientemente amplia como para las tareas comunes sin verificación de rango. Esto conduce 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 correcto porquen
se supone <INT_MAX
y las cadenas nunca serán demasiado largas, en lugar de estar protegidas por completo en el primer caso o usarsize_t
,unsigned
o inclusolong long
en el segundo.C / C ++ se desarrolló en una era que incluía 16 bits y 32 bits,
int
y el bit adicional que ofrece un 16 bits sin firmarsize_t
fue significativo. Se necesitaba la atención en lo que se refiere a desbordarse problemas ya seaint
ounsigned
.Con aplicaciones de 32 bits (o más amplias) de Google en
int/unsigned
plataformas que no son de 16 bits , brinda la falta de atención al desbordamiento de +/-int
dada su amplia gama. Esto tiene sentido para que dichas aplicaciones fomentenint
el cambiounsigned
. Sin embargo, lasint
matemáticas no están bien protegidas.Las
int/unsigned
preocupaciones estrechas de 16 bits 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 amplio rango de código C / C ++.
En C / C ++, el desbordamiento matemático 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 sin firmar .
Como bien comentó @Chris Uzdavinis , es mejor evitar mezclar firmado y no firmado por todos (especialmente los principiantes) y codificado cuidadosamente cuando sea necesario.
fuente
int
tampoco modela el comportamiento de un entero "real". El comportamiento indefinido en el desbordamiento no es lo que un matemático piensa de los números enteros: no hay posibilidad de "desbordamiento" con un entero abstracto. Pero estas son unidades de almacenamiento de máquinas, no números de matemáticos.signed int
desbordamiento es fácil de detectar (con-ftrapv
), mientras que el "desbordamiento" sin firmar es difícil de detectar.Tengo algo de experiencia con la guía de estilo de Google, también conocida como la Guía del autoestopista sobre las directivas locas de los malos programadores que entraron en la empresa hace mucho, mucho tiempo. Esta pauta en particular es solo un ejemplo de las docenas de reglas locas en ese libro.
Los errores solo ocurren con tipos sin firmar si intenta hacer aritmética con ellos (vea el ejemplo de Chris Uzdavinis arriba), en otras palabras, si los usa como números. Los tipos sin firmar no están destinados a almacenar cantidades numéricas, están destinados a 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 una idiotez. ¿Usarías un doble para almacenar el tamaño de una lista también? Que haya personas en Google que almacenen tamaños de contenedores usando tipos aritméticos y requieran que otros hagan lo mismo dice algo sobre la empresa. Una cosa que noto acerca de tales dictados es que cuanto más tontos son, más deben ser reglas estrictas de "hazlo o te despiden" porque, de lo contrario, las personas con sentido común ignorarían la regla.
fuente
unsigned
tipos solo pudieran contener recuentos y no se usarían en aritmética. Así que la parte "Insane Directives from Bad Programmers" tiene más sentido.int
era de 16 bits, pero mucho menos hoy) es mejor tener recuentos que se comporten como números.Usando tipos sin firmar para representar valores no negativos ...
Las Pautas de codificación de Google ponen énfasis en el primer tipo de consideración. Otros conjuntos de pautas, como las Pautas principales de C ++ , ponen más énfasis en el segundo punto. Por ejemplo, considere la Directriz básica I.12 :
Por supuesto, podría argumentar a favor de un
non_negative
contenedor para enteros, que evite ambas categorías de errores, pero eso tendría sus propios problemas ...fuente
La declaración de Google trata sobre el uso de unsigned como tipo de tamaño para contenedores . Por el contrario, la pregunta parece ser más general. Por favor, tenlo en cuenta mientras sigues leyendo.
Dado que la mayoría de las respuestas hasta ahora reaccionaron a la declaración de Google, menos a la pregunta más importante, comenzaré mi respuesta sobre los tamaños negativos de los contenedores y, posteriormente, intentaré convencer a cualquiera (sin esperanza, lo sé ...) de que sin firmar es bueno.
Tamaños de contenedores firmados
Supongamos que alguien codificó un error, lo que da como resultado un índice de contenedor negativo. El resultado es un comportamiento indefinido o una excepción / infracción de acceso. ¿Es eso realmente mejor que obtener un comportamiento indefinido o una excepción / violación de acceso cuando el tipo de índice no estaba firmado? Creo que no.
Ahora, hay una clase de gente a la que le encanta hablar de matemáticas y lo que es "natural" en este contexto. ¿Cómo puede ser natural que un tipo integral con número negativo describa algo, que es inherentemente> = 0? ¿Usas mucho matrices con tamaños negativos? En mi humilde opinión, especialmente a las personas con inclinaciones matemáticas les resultaría irritante este desajuste de semántica (el tipo de tamaño / índice dice que es posible lo negativo, mientras que una matriz de tamaño negativo es difícil de imaginar).
Entonces, la única pregunta que queda sobre este asunto es si, como se indica en el comentario de Google, un compilador podría ayudar activamente a encontrar tales errores. E incluso mejor que la alternativa, que serían enteros sin firmar protegidos por subdesbordamiento (el ensamblaje x86-64 y probablemente otras arquitecturas tienen medios para lograr eso, solo C / C ++ no usa esos medios). La única forma que puedo comprender es si el compilador agregó automáticamente comprobaciones de tiempo de ejecución (
if (index < 0) throwOrWhatever
) o en caso de que las acciones de tiempo de compilación produzcan muchas advertencias / errores potencialmente falsos positivos "El índice para este acceso a la matriz podría ser negativo". Tengo mis dudas, esto sería de gran ayuda.Además, las personas que realmente escriben verificaciones en tiempo de ejecución para sus índices de matriz / contenedor, es más trabajo tratar con enteros firmados. En lugar de escribir
if (index < container.size()) { ... }
ahora tiene que escribir:if (index >= 0 && index < container.size()) { ... }
. Me parece un trabajo forzado y no una mejora ...Los idiomas sin tipos sin firmar apestan ...
Sí, esta es una puñalada en Java. Ahora, vengo de una experiencia en programación integrada y trabajamos mucho con buses de campo, donde las operaciones binarias (y, o, xor, ...) y la composición de valores a nivel de bits es literalmente el pan y la mantequilla. Para uno de nuestros productos, nosotros, o más bien un cliente, queríamos un puerto java ... y me senté frente al tipo afortunadamente muy competente que hizo el puerto (me negué ...). Trató de mantener la compostura ... y sufrir en silencio ... pero el dolor estaba ahí, no podía dejar de maldecir después de unos días de lidiar constantemente con valores integrales firmados, los cuales DEBERÍAN estar sin firmar ... Incluso escribiendo pruebas unitarias para esos escenarios son dolorosos y yo, personalmente, creo que Java habría estado mejor si hubieran omitido los enteros con signo y solo se hubieran ofrecido sin firmar ... al menos entonces, no tiene que preocuparse por las extensiones de signo, etc.
Esos son mis 5 centavos al respecto.
fuente