Uso de enteros sin signo en C y C ++

23

Tengo una pregunta muy simple que me desconcierta durante mucho tiempo. Estoy tratando con redes y bases de datos, por lo que una gran cantidad de datos con los que estoy contando son contadores de 32 y 64 bits (sin signo), identificadores de identificación de 32 y 64 bits (tampoco tienen un mapeo significativo para el signo). Prácticamente nunca trato con ningún asunto de palabras reales que pueda expresarse como un número negativo.

Mis compañeros de trabajo y yo usamos rutinariamente tipos sin signo como uint32_ty uint64_tpara estos asuntos y debido a que sucede con tanta frecuencia, también los usamos para índices de matriz y otros usos enteros comunes.

Al mismo tiempo, varias guías de codificación que estoy leyendo (por ejemplo, Google) desalientan el uso de tipos enteros sin signo, y que yo sepa, ni Java ni Scala tienen tipos enteros sin signo.

Por lo tanto, no pude averiguar qué es lo correcto: usar valores con signo en nuestro entorno sería muy inconveniente, al mismo tiempo que codificaría las guías para insistir en hacer exactamente esto.

zzz777
fuente

Respuestas:

31

Hay dos escuelas de pensamiento sobre esto, y ninguna de las dos estará de acuerdo.

El primero argumenta que hay algunos conceptos que son inherentemente sin signo, como los índices de matriz. No tiene sentido usar números con signo para aquellos, ya que puede conducir a errores. También puede imponer límites innecesarios a las cosas: una matriz que utiliza índices de 32 bits con signo solo puede acceder a 2 mil millones de entradas, mientras que cambiar a números de 32 bits sin signo permite 4 mil millones de entradas.

El segundo argumenta que en cualquier programa que use números sin signo, tarde o temprano terminarás haciendo aritmética mixta con signo y sin signo. Esto puede dar resultados extraños e inesperados: convertir un gran valor sin signo en firmado da un número negativo y, por el contrario, emitir un número negativo en sin signo da un número positivo grande. Esto puede ser una gran fuente de errores.

Simon B
fuente
8
El compilador detecta problemas aritméticos con signo y sin signo mixtos; solo mantenga su compilación sin advertencia (con un nivel de advertencia lo suficientemente alto). Además, intes más corto de escribir :)
rucamzu
77
Confesión: estoy con la segunda escuela de pensamiento, y aunque entiendo las consideraciones para los tipos sin signo: intes más que suficiente para los índices de matriz el 99,99% de las veces. Los problemas aritméticos con signo sin signo son mucho más comunes y, por lo tanto, tienen prioridad en términos de qué evitar. Sí, los compiladores le advierten sobre esto, pero ¿cuántas advertencias recibe al compilar cualquier proyecto considerable? Ignorar las advertencias es peligroso y una mala práctica, pero en el mundo real ...
Elias Van Ootegem
11
+1 a la respuesta. Precaución : opiniones contundentes a continuación : 1: Mi respuesta a la segunda escuela de pensamiento es: apostaría dinero a que cualquiera que obtenga resultados inesperados de tipos integrales sin signo en C tendrá un comportamiento indefinido (y no del tipo puramente académico) en sus programas C no triviales que usan tipos integrales con signo . Si no conoce C lo suficientemente bien como para pensar que los tipos sin signo son los mejores para usar, le aconsejo que evite C. 2: Hay exactamente un tipo correcto para los índices y tamaños de matriz en C, y eso es size_t, a menos que haya un caso especial Buena razón de lo contrario.
mtraceur
55
Te encuentras en problemas sin una firma mixta. Simplemente calcule unsigned int menos unsigned int.
gnasher729
44
Simon, que no está en desacuerdo contigo, solo con la primera escuela de pensamiento que argumenta que "hay algunos conceptos inherentemente sin signo, como los índices de matriz". específicamente: "Hay exactamente un tipo correcto para los índices de matriz ... en C", ¡ Bullshit! . Los DSPers usamos índices negativos todo el tiempo. particularmente con respuestas de impulso de simetría par o impar que no son causales. y para matemáticas LUT. estoy en la segunda escuela de pensamiento, pero yo creo que es útil tener dos números enteros con y sin signo en C y C ++.
Robert Bristow-Johnson
21

En primer lugar, la directriz de codificación de Google C ++ no es muy buena para seguir: evita cosas como excepciones, impulso, etc., que son elementos básicos de C ++ moderno. En segundo lugar, el hecho de que una determinada directriz funcione para la empresa X no significa que sea la adecuada para usted. Seguiría usando tipos sin signo, ya que los necesita.

Una regla general decente para C ++ es: preferir a intmenos que tenga una buena razón para usar otra cosa.

bstamour
fuente
8
Eso no es lo que quiero decir en absoluto. Los constructores son para establecer invariantes, y dado que no son funciones, no pueden simplemente return falsesi ese invariante no está establecido. Por lo tanto, puede separar cosas y usar funciones de inicio para sus objetos, o puede lanzar un std::runtime_error, dejar que se produzca el desbobinado de la pila y dejar que todos sus objetos RAII se limpien automáticamente y usted, el desarrollador, puede manejar la excepción donde sea conveniente para que lo hagas
bstamour
55
No veo cómo el tipo de aplicación hace la diferencia. Cada vez que llama a un constructor sobre un objeto, está estableciendo un invariante con los parámetros. Si esa invariante no se puede cumplir, entonces debe indicar un error; de lo contrario, su programa no estará en buen estado. Como los constructores no pueden devolver una bandera, lanzar una excepción es una opción natural. Dé un argumento sólido sobre por qué una aplicación comercial no se beneficiaría de este estilo de codificación.
bstamour
8
Dudo mucho que la mitad de todos los programadores de C ++ sean incapaces de usar excepciones correctamente. Pero de todos modos, si crees que tus compañeros de trabajo son incapaces de escribir C ++ moderno, mantente alejado de C ++ moderno.
bstamour
66
@ zzz777 ¿No utiliza excepciones? Tenga constructores privados que estén envueltos por funciones de fábrica públicas que capturen las excepciones y hagan qué: devolver a nullptr? devolver un objeto "predeterminado" (lo que sea que eso signifique)? No resolvió nada: acaba de ocultar el problema debajo de una alfombra y espera que nadie se entere.
Mael
55
@ zzz777 Si vas a bloquear la caja de todos modos, ¿por qué te importa si sucede por una excepción o signal(6)? Si usa una excepción, el 50% de los desarrolladores que saben cómo tratar con ellos pueden escribir un buen código, y el resto puede ser llevado por sus pares.
IllusiveBrian
6

Las otras respuestas carecen de ejemplos del mundo real, por lo que agregaré uno. Una de las razones por las que (personalmente) trato de evitar los tipos sin firmar.

Considere usar size_t estándar como índice de matriz:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, perfectamente normal. Luego, considere que decidimos cambiar la dirección del bucle por alguna razón:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

Y ahora no funciona. Si lo usáramos intcomo iterador, no habría problema. He visto ese error dos veces en los últimos dos años. Una vez sucedió en producción y fue difícil de depurar.

Otra razón para mí son las advertencias molestas, que te hacen escribir algo así cada vez :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Estas son cosas menores, pero suman. Siento que el código es más limpio si solo se usan enteros con signo en todas partes.

Editar: Claro, los ejemplos parecen tontos, pero vi a personas cometer este error. Si hay una manera tan fácil de evitarlo, ¿por qué no usarlo?

Cuando compilo el siguiente código con VS2015 o GCC, no veo advertencias con la configuración de advertencia predeterminada (incluso con -Wall para GCC). Debe solicitar -Wextra para recibir una advertencia sobre esto en GCC. Esta es una de las razones por las que siempre debe compilar con Wall y Wextra (y usar un analizador estático), pero en muchos proyectos de la vida real la gente no hace eso.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
fuente
Puede equivocarse aún más con los tipos firmados ... Y su código de ejemplo es tan cerebral y terriblemente incorrecto que cualquier compilador decente le advertirá si solicita advertencias.
Deduplicador
1
En el pasado he recurrido a tales horrores for (size_t i = n - 1; i < n; --i)para que funcione correctamente.
Simon B
2
Hablando de bucles for size_ten reversa, hay una guía de codificación al estilo defor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong
2
@rwong Omg, esto es feo. ¿Por qué no solo usar int? :)
Aleksei Petrenko
1
@AlexeyPetrenko: tenga en cuenta que ni los estándares actuales de C ni C ++ garantizan que intsea ​​lo suficientemente grande como para contener todos los valores válidos de size_t. En particular, intpuede permitir números solo hasta 2 ^ 15-1, y comúnmente lo hace en sistemas que tienen límites de asignación de memoria de 2 ^ 16 (o en algunos casos incluso más). longpuede ser una apuesta más segura, aunque todavía no se garantiza que funcione. Solo size_tse garantiza que funcionará en todas las plataformas y en todos los casos.
Julio
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

El problema aquí es que usted escribió el ciclo de una manera no inteligente que conduce a un comportamiento erróneo. La construcción del bucle es como si los principiantes lo aprendieran para los tipos con signo (que está bien y es correcto) pero simplemente no se ajusta a los valores sin signo. Pero esto no puede servir como contraargumento contra el uso de tipos sin signo, la tarea aquí es simplemente acertar. Y esto se puede solucionar fácilmente para que funcione de manera confiable para tipos sin signo de la siguiente manera:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Este cambio simplemente revierte la secuencia de la operación de comparación y decremento y es, en mi opinión, la forma más efectiva, tranquila, limpia y corta para manejar contadores sin firmar en bucles hacia atrás. Harías lo mismo (intuitivamente) cuando uses un ciclo while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

No puede ocurrir un desbordamiento, el caso de un contenedor vacío está cubierto implícitamente, como en la variante bien conocida para el bucle de contador firmado, y el cuerpo del bucle puede permanecer inalterado en comparación con un contador firmado o un bucle de avance. Solo tiene que acostumbrarse a la primera construcción de bucle de aspecto algo extraño. Pero después de haber visto eso una docena de veces, ya no hay nada ininteligible.

Tendría suerte si los cursos para principiantes no solo mostraran el bucle correcto para los tipos firmados sino también para los no firmados. Esto evitaría un par de errores que, en mi humilde opinión, se debe culpar a los desarrolladores involuntarios en lugar de culpar al tipo sin firmar.

HTH

Don pedro
fuente
1

Los enteros sin signo están ahí por una razón.

Considere, por ejemplo, la entrega de datos como bytes individuales, por ejemplo, en un paquete de red o un búfer de archivo. Ocasionalmente puede encontrar bestias como números enteros de 24 bits. Fácilmente desplazado de tres enteros sin signo de 8 bits, no es tan fácil con enteros con signo de 8 bits.

O piense en algoritmos que usan tablas de búsqueda de caracteres. Si un carácter es un entero sin signo de 8 bits, puede indexar una tabla de búsqueda por un valor de carácter. Sin embargo, ¿qué haces si el lenguaje de programación no admite enteros sin signo? Tendría índices negativos para una matriz. Bueno, supongo que podrías usar algo como charval + 128eso, pero eso es feo.

Muchos formatos de archivo, de hecho, usan enteros sin signo y si el lenguaje de programación de la aplicación no admite enteros sin signo, eso podría ser un problema.

Luego considere los números de secuencia TCP. Si escribe cualquier código de procesamiento TCP, definitivamente querrá usar enteros sin signo.

A veces, la eficiencia es tan importante que realmente necesitas ese número extra de enteros sin signo. Considere, por ejemplo, los dispositivos IoT que se envían en millones. Muchos recursos de programación pueden justificarse para gastarse en micro optimizaciones.

Yo diría que la compilación con advertencias adecuadas puede superar la justificación para evitar el uso de tipos enteros sin signo (aritmética de signos mixtos, comparaciones de signos mixtos). Dichas advertencias generalmente no están habilitadas de forma predeterminada, pero vea, por ejemplo, -Wextrao por separado -Wsign-compare(habilitado automáticamente en C por -Wextra, aunque no creo que esté habilitado automáticamente en C ++) y -Wsign-conversion.

Sin embargo, en caso de duda, utilice un tipo con signo. Muchas veces, es una elección que funciona bien. ¡Y habilite esas advertencias del compilador!

juhist
fuente
0

Hay muchos casos en los que los enteros en realidad no representan números, pero, por ejemplo, una máscara de bits, una identificación, etc. Básicamente, los casos en que agregar 1 a un entero no tienen ningún resultado significativo. En esos casos, use sin firmar.

Hay muchos casos en los que haces aritmética con enteros. En estos casos, use enteros con signo, para evitar el mal comportamiento alrededor de cero. Vea muchos ejemplos con bucles, donde ejecutar un bucle a cero usa un código muy poco intuitivo o se rompe debido al uso de números sin signo. Existe el argumento "pero los índices nunca son negativos", claro, pero las diferencias de índices, por ejemplo, son negativas.

En el caso muy raro de que los índices excedan 2 ^ 31 pero no 2 ^ 32, no usa enteros sin signo, usa enteros de 64 bits.

Finalmente, una buena trampa: en un bucle "for (i = 0; i <n; ++ i) a [i] ..." si no estoy firmado 32 bits y la memoria excede las direcciones de 32 bits, el compilador no puede optimizar el acceso a a [i] incrementando un puntero, porque en i = 2 ^ 32 - 1 me envuelve. Incluso cuando n nunca crece tanto. El uso de enteros con signo evita esto.

gnasher729
fuente
-5

Finalmente, encontré una muy buena respuesta aquí: "Libro de cocina de programación segura" de J.Viega y M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Problemas de seguridad con enteros firmados:

  1. Si la función requiere un parámetro positivo, es fácil olvidar comprobar el rango inferior.
  2. Patrón de bits no intuitivo de conversiones de tamaño entero negativo.
  3. Patrón de bits no intuitivo producido por la operación de desplazamiento a la derecha de un entero negativo.

Existen problemas con las conversiones con signo <-> sin signo, por lo que no es aconsejable utilizar mix.

zzz777
fuente
1
¿Por qué es una buena respuesta? ¿Qué es la receta 3.5? ¿Qué dice sobre el desbordamiento de enteros, etc.?
Baldrickk
En mi experiencia práctica, es un libro muy bueno con valiosos consejos sobre todos los demás aspectos que probé y es bastante firme en esta recomendación. En comparación con los peligros de desbordamientos de enteros en matrices de más de 4G, parece bastante débil. Si tengo que lidiar con matrices así de grandes, mi programa tendrá muchos ajustes para evitar penalizaciones de rendimiento.
zzz777
1
no se trata de si el libro es bueno. Su respuesta no proporciona ninguna justificación para el uso del destinatario, y no todos tendrán una copia del libro para buscarlo. Mire los ejemplos de cómo escribir una buena respuesta
Baldrickk
Para su información, acabo de enterarme de otra razón para usar enteros sin signo: uno puede detectar fácilmente overlow: youtube.com/…
zzz777