¿Cuáles son las mejores prácticas con respecto a las entradas sin firmar?

43

Uso ints sin firmar en todas partes, y no estoy seguro de si debería hacerlo. Esto puede ser desde columnas de identificación de clave primaria de la base de datos hasta contadores, etc. Si un número nunca debe ser negativo, siempre usaré un int sin signo.

Sin embargo, noto en el código de otros que nadie más parece hacer esto. ¿Hay algo crucial que estoy pasando por alto?

Editar: desde esta pregunta también he notado que en C, devolver valores negativos para errores es común en lugar de arrojar excepciones como en C ++.

wting
fuente
26
Solo ten cuidado for(unsigned int n = 10; n >= 0; n --)(bucles infinitos)
Chris Burt-Brown
3
En C y C ++, las entradas sin signo tienen un comportamiento de desbordamiento definido con precisión (módulo 2 ^ n). Los signos firmados no. Los optimizadores explotan cada vez más ese comportamiento de desbordamiento indefinido, lo que lleva a resultados sorprendentes en algunos casos.
Steve314
2
¡Buena pregunta! Yo también estuve una vez tentado a usar el rango restringido de uints t pero descubrí que el riesgo / inconveniente superaba cualquier beneficio / conveniencia. La mayoría de las bibliotecas, como dijiste, aceptan entradas regulares donde lo haría un uint. Esto hace que sea difícil trabajar con él, pero también plantea la pregunta: ¿vale la pena? En la práctica (suponiendo que no hace las cosas de una manera tonta), rara vez tendrá un valor de -218 donde se espera uno positivo. Ese -218 debe haber venido de alguna parte, ¿verdad? y puedes rastrear su origen. Sucede raramente. Utilice afirmaciones, excepciones, contratos de código para ayudarlo.
Trabajo
@William Ting: Si se trata solo de C / C ++, debe agregar las etiquetas apropiadas a su pregunta.
CesarGon
2
@ Chris: ¿Cuán significativo es el problema del bucle infinito en la realidad? Quiero decir, si llega a ser lanzado, entonces el código obviamente no fue probado. Incluso cuando necesite algunas horas para depurarlo la primera vez que cometa este error, la segunda vez debe saber qué buscar primero cuando su código no deja de repetirse.
Seguro el

Respuestas:

28

¿Hay algo crucial que estoy pasando por alto?

Cuando los cálculos involucran tipos con y sin signo, así como diferentes tamaños, las reglas para la promoción de tipos pueden ser complejas y conducir a un comportamiento inesperado .

Creo que esta es la razón principal por la que Java omitió los tipos int sin signo.

Michael Borgwardt
fuente
3
Otra solución sería exigirle que emite manualmente sus números según corresponda. Esto es lo que parece hacer Go (aunque solo he jugado un poco), y me gusta más que el enfoque de Java.
Tikhon Jelvis
2
Esa fue una buena razón para que Java no incluyera el tipo sin signo de 64 bits, y quizás una razón decente para no incluir un tipo sin signo de 32 bits [aunque la semántica de agregar valores de 32 bits con signo y sin signo no sería difícil-- tal operación simplemente debería producir un resultado firmado de 64 bits]. intSin embargo, los tipos sin signo más pequeños que no plantearían tal dificultad (ya que cualquier cálculo promoverá int); No tengo nada bueno que decir sobre la falta de un tipo de byte sin signo.
supercat
17

Creo que Michael tiene un punto válido, pero en mi opinión, la razón por la que todos usan int todo el tiempo (especialmente en for (int i = 0; i < max, i++) es que lo aprendimos de esa manera. Cuando cada ejemplo en un libro de " cómo aprender programación " se usa inten un forbucle, muy pocos cuestionarán esa práctica.

La otra razón es que intes un 25% más corta que todos uint, y todos somos flojos ... ;-)

Treb
fuente
2
Estoy de acuerdo con el tema educativo. La mayoría de las personas parecen nunca cuestionar lo que leen: si está en un libro, no puede estar equivocado, ¿verdad?
Matthieu M.
1
Supuestamente, esa es también la razón por la que todos usan postfix ++cuando se incrementan, a pesar de que su comportamiento particular rara vez es necesario e incluso podría dar lugar a cambios innecesarios sobre las copias si el índice de bucle es un iterador u otro tipo no fundamental (o el compilador es realmente denso) .
underscore_d
Simplemente no haga algo como "for (uint i = 10; i> = 0; --i)". Usar solo ints para variables de bucle evita esta posibilidad.
David Thornley
8

Mezclar tipos con y sin signo puede llevarte a un mundo de dolor. Y no puede usar todos los tipos sin signo porque encontrará cosas que tienen un rango válido que incluye números negativos o necesitan un valor para indicar un error y -1 es lo más natural. Entonces, el resultado neto es que muchos programadores usan todos los tipos de enteros con signo.

David Schwartz
fuente
1
Tal vez sea una mejor práctica no mezclar valores válidos con indicación de error en la misma variable y usar variables separadas para esto. Por supuesto, la biblioteca estándar de C no es un buen ejemplo aquí.
Seguro el
7

Para mí, los tipos tienen mucho que ver con la comunicación. Al usar explícitamente un int sin signo, me dice que los valores con signo no son valores válidos. Esto me permite agregar información al leer su código además del nombre de la variable. Idealmente, un tipo no anónimo me diría más, pero me da más información que si hubiera usado ints en todas partes.

Desafortunadamente, no todo el mundo es muy consciente de lo que comunica su código, y esa es probablemente la razón por la que ve entradas en todas partes, aunque los valores no estén firmados.

daramarak
fuente
44
Pero es posible que desee restringir mis valores durante un mes solo del 1 al 12. ¿Utilizo otro tipo para ello? ¿Qué tal un mes? Algunos idiomas en realidad permiten restringir valores como ese. Otros, como .Net / C # proporcionan contratos de código. Claro, los enteros no negativos ocurren con bastante frecuencia, pero la mayoría de los idiomas que admiten este tipo no admiten restricciones adicionales. Entonces, ¿debería uno usar una combinación de uints y verificación de errores, o simplemente hacer todo a través de la verificación de errores? La mayoría de las bibliotecas no preguntan dónde debería tener sentido usar uno, por lo tanto, usar uno y la conversión pueden ser inconvenientes.
Trabajo
@ Job Yo diría que deberías usar algún tipo de restricción impuesta por el compilador / intérprete en tus meses. Es posible que le dé algo de preparación para configurar, pero para el futuro tiene una restricción forzada que evita errores y comunica mucho más claramente lo que espera. Prevenir errores y facilitar la comunicación son mucho más importantes que los inconvenientes durante la implementación.
daramarak
1
"Es posible que desee restringir mis valores de un mes a 1 a 12 solamente" Si tiene un conjunto finito de valores como meses, debe usar un tipo de enumeración, no enteros sin formato.
Josh Caswell el
6

Utilizo unsigned inten C ++ para los índices de matriz, principalmente, y para cualquier contador que comience desde 0. Creo que es bueno decir explícitamente "esta variable no puede ser negativa".

cuant_dev
fuente
14
Probablemente deberías estar usando size_t para esto en c ++
JohnB
2
Lo sé, simplemente no me molestan.
quant_dev
3

Debería preocuparse por esto cuando se trata de un número entero que realmente podría acercarse o exceder los límites de un int firmado. Como el máximo positivo de un entero de 32 bits es 2,147,483,647, debe usar un int sin signo si sabe que a) nunca será negativo yb) podría llegar a 2,147,483,648. En la mayoría de los casos, incluidas las claves y los contadores de la base de datos, nunca me acercaré a este tipo de números, así que no me preocupo por preocuparme si el bit de signo se usa para un valor numérico o para indicar el signo.

Yo diría: use int a menos que sepa que necesita un int sin firmar.

Joel Etherton
fuente
2
Cuando trabaje con valores que puedan alcanzar los valores máximos, debe comenzar a verificar las operaciones para desbordamientos de enteros, independientemente del signo. Estas comprobaciones suelen ser más fáciles para los tipos sin firmar, porque la mayoría de las operaciones tienen resultados bien definidos sin un comportamiento indefinido y definido por la implementación.
Seguro el
3

Es una compensación entre simplicidad y confiabilidad. Cuantos más errores se puedan detectar en el momento de la compilación, más confiable será el software. Diferentes personas y organizaciones están en diferentes puntos a lo largo de ese espectro.

Si alguna vez realiza una programación de alta confiabilidad en Ada, incluso utiliza diferentes tipos para variables como la distancia en pies frente a la distancia en metros, y el compilador lo marca si accidentalmente se asigna uno al otro. Eso es perfecto para programar un misil guiado, pero exagerado (juego de palabras) si está validando un formulario web. No hay necesariamente nada malo en ninguna de las formas, siempre y cuando cumpla con los requisitos.

Karl Bielefeldt
fuente
2

Me inclino a estar de acuerdo con el razonamiento de Joel Etherton, pero llego a la conclusión opuesta. A mi modo de ver, incluso si sabe que es poco probable que los números se acerquen a los límites de un tipo con signo, si sabe que los números negativos no sucederán, entonces hay muy pocas razones para usar la variante con signo de un tipo.

Por la misma razón por la que, en algunas instancias seleccionadas, he usado BIGINT(entero de 64 bits) en lugar de INTEGER(entero de 32 bits) en las tablas de SQL Server. La probabilidad de que los datos alcancen el límite de 32 bits dentro de un período de tiempo razonable es minúscula, pero si sucede, las consecuencias en algunas situaciones podrían ser bastante devastadoras. Solo asegúrate de mapear los tipos entre idiomas correctamente, o terminarás con una rareza interesante en el futuro ...

Dicho esto, para algunas cosas, como los valores de clave primaria de la base de datos, con o sin signo, realmente no importa, porque a menos que esté reparando manualmente datos rotos o algo por el estilo, nunca estará tratando con el valor directamente; Es un identificador, nada más. En esos casos, la consistencia es probablemente más importante que la elección exacta de la firma. De lo contrario, terminará con algunas columnas de clave externa que están firmadas y otras que no están firmadas, sin ningún patrón aparente, o esa extraña rareza nuevamente.

un CVn
fuente
Si está trabajando con datos extraídos de un sistema SAP, le recomiendo BIGINT para los campos de ID (como CustomerNumber, ArticleNumber, etc.). Mientras nadie use cadenas alfanuméricas como ID, eso es ... suspiro
Treb
1

Recomendaría que fuera de los contextos de almacenamiento de datos y de intercambio de datos con limitaciones de espacio, generalmente se usen tipos con signo. En la mayoría de los casos en los que un entero con signo de 32 bits sería demasiado pequeño, pero un valor sin signo de 32 bits sería suficiente por hoy, no pasará mucho tiempo antes de que el valor sin signo de 32 bits tampoco sea lo suficientemente grande.

Los tiempos principales en los que uno debe usar tipos sin signo son cuando uno está ensamblando múltiples valores en uno más grande (por ejemplo, convirtiendo cuatro bytes en un número de 32 bits) o descomponiendo valores más grandes en valores más pequeños (por ejemplo, almacenando un número de 32 bits como cuatro bytes) ), o cuando se tiene una cantidad que se espera que "se transfiera" periódicamente y se necesite tratar con ella (piense en un medidor de servicios residenciales; la mayoría de ellos tienen suficientes dígitos para asegurarse de que no se acumularán entre lecturas si se leen tres veces al año, pero no lo suficiente como para garantizar que no se vuelquen dentro de la vida útil del medidor). Los tipos sin signo a menudo tienen suficiente "rareza" que solo deberían usarse en casos donde su semántica es necesaria.

Super gato
fuente
1
"Recomendaría generalmente [...] usar tipos con signo". Hm, olvidó mencionar las ventajas de los tipos con signo y solo dio una lista de cuándo usar tipos sin signo. "rareza" ? Si bien la mayoría de las operaciones sin firmar tienen un comportamiento y resultados bien definidos, se ingresa un comportamiento indefinido y definido en la implementación cuando se usan tipos con signo (desbordamiento, cambio de bits, ...). Tienes una extraña definición de "rareza" aquí.
Seguro el
1
@Secure: La "rareza" a la que me refiero tiene que ver con la semántica de los operadores de comparación, especialmente en operaciones que involucran tipos mixtos con y sin signo. Tiene razón en que el comportamiento de los tipos con signo no está definido cuando se usan valores lo suficientemente grandes como para desbordarse, pero el comportamiento de los tipos sin signo puede ser sorprendente incluso cuando se trata de números relativamente pequeños. Por ejemplo, (-3) + (1u) es mayor que -1. Además, algunas relaciones asociativas matemáticas normales que se aplicarían a los números no se aplican a los no firmados. Por ejemplo, (ab)> c no implica (ac)> b.
supercat
1
@Secure: Si bien es cierto que tampoco siempre se puede confiar en un comportamiento asociativo con números con signo "grandes", los comportamientos funcionan según lo esperado cuando se trata de números que son "pequeños" en relación con el dominio de los números enteros con signo. Por el contrario, la no asociación mencionada anteriormente es problemática con los valores sin signo "2 3 1". Por cierto, el hecho de que los comportamientos firmados tengan un comportamiento indefinido cuando se usan fuera de los límites puede permitir una generación de código mejorada en algunas plataformas cuando se usan valores más pequeños que el tamaño de la palabra nativa.
supercat
1
Si estos comentarios hubieran estado en su respuesta en primer lugar, en lugar de una recomendación y "insultos" sin dar ninguna razón, no lo habría comentado. ;) Aunque todavía no estoy de acuerdo con la "rareza" aquí, es simplemente la definición del tipo. Utilice la herramienta adecuada para el trabajo dado y conozca la herramienta, por supuesto. Los tipos sin signo son la herramienta incorrecta cuando necesita relaciones +/-. Hay una razón por la cual size_tno está firmado y ptrdiff_testá firmado.
Seguro el
1
@Secure: si lo que uno quiere es representar una secuencia de bits, los tipos sin signo son geniales; Creo que estamos de acuerdo allí. Y en algunos micros pequeños, los tipos sin signo pueden ser más eficientes para cantidades numéricas. También son útiles en los casos en que los deltas representan cantidades numéricas pero los valores reales no lo hacen (por ejemplo, números de secuencia TCP). Por otro lado, cada vez que se restan valores sin signo, uno tiene que preocuparse por los casos de esquina, incluso cuando los números son pequeños; tales matemáticas con valores con signo solo presentan casos de esquina cuando los números son grandes.
supercat
1

Utilizo ints sin firmar para aclarar mi código y su intención. Una cosa que hago para protegerme de las conversiones implícitas inesperadas cuando hago aritmética con tipos con signo y sin signo es usar un short sin signo (generalmente 2 bytes) para mis variables sin signo. Esto es efectivo por un par de razones:

  • Cuando hace aritmética con sus variables cortas y literales sin signo (que son de tipo int) o variables de tipo int, esto asegura que la variable sin signo siempre se promocionará a int antes de evaluar la expresión, ya que int siempre tiene un rango más alto que short . Esto evita cualquier comportamiento inesperado al hacer aritmética con tipos con signo y sin signo, suponiendo que el resultado de la expresión se ajuste a un int con signo, por supuesto.
  • La mayoría de las veces, las variables sin signo que está usando no excederán el valor máximo de un corto de 2 bytes sin signo (65,535)

El principio general es que el tipo de sus variables sin signo debe tener un rango más bajo que el tipo de las variables con signo para garantizar la promoción al tipo con signo. Entonces no tendrá ningún comportamiento de desbordamiento inesperado. Obviamente, no puede garantizar esto todo el tiempo, pero (la mayoría de las veces) es factible garantizarlo.

Por ejemplo, recientemente tuve un bucle for algo como esto:

const unsigned short cuint = 5;
for(unsigned short i=0; i<10; ++i)
{
    if((i-2)%cuint == 0)
    {
       //Do something
    }
}

El literal '2' es de tipo int. Si yo fuera un unsigned int en lugar de un unsigned short, entonces en la sub-expresión (i-2), 2 se promocionaría a unsigned int (dado que unsigned int tiene una prioridad más alta que la firma int). Si i = 0, entonces la sub-expresión es igual a (0u-2u) = algún valor masivo debido al desbordamiento. La misma idea con i = 1. Sin embargo, dado que i es un corto sin signo, se promociona al mismo tipo que el literal '2', que está firmado int, y todo funciona bien.

Para mayor seguridad: en el raro caso en el que la arquitectura que está implementando hace que int sea de 2 bytes, esto podría hacer que ambos operandos en la expresión aritmética se promocionen a unsigned int en el caso en que la variable corta sin signo no encaje en el int de 2 bytes firmado, el último de los cuales tiene un valor máximo de 32,767 <65,535. (Consulte https://stackoverflow.com/questions/17832815/c-implicit-conversion-signed-unsigned para obtener más detalles). Para protegerse de esto, simplemente puede agregar un static_assert a su programa de la siguiente manera:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

y no se compilará en arquitecturas donde int es de 2 bytes.

AlmiranteAdama
fuente