Para el código incrustado, ¿por qué debo usar los tipos "uint_t" en lugar de "int sin firmar"?

20

Estoy escribiendo una aplicación en c para un STM32F105, usando gcc.

En el pasado (con proyectos más simples), siempre he definido las variables como char , int , unsigned int , etc.

Veo que es común usar los tipos definidos en stdint.h, como int8_t , uint8_t , uint32_t , etc. Esto es cierto en las múltiples API que estoy usando, y también en el Biblioteca ARM CMSIS de ST.

Creo que entiendo por qué debemos hacerlo; para permitir que el compilador optimice mejor el espacio de memoria. Supongo que puede haber razones adicionales.

Sin embargo, debido a las reglas de promoción de enteros de c, me mantengo contra las advertencias de conversión cada vez que intento agregar dos valores, hacer una operación a nivel de bits, etc. La advertencia dice algo así como conversion to 'uint16_t' from 'int' may alter its value [-Wconversion] . El problema se trata aquí y aquí .

No sucede cuando se utilizan variables declaradas como int o unsigned int .

Para dar un par de ejemplos, dado esto:

uint16_t value16;
uint8_t value8;

Tendría que cambiar esto:

value16 <<= 8;
value8 += 2;

a esto:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

Es feo, pero puedo hacerlo si es necesario. Aquí están mis preguntas:

  1. ¿Hay un caso en el que la conversión de unsigned a firmada y volver a unsigned hará que el resultado sea incorrecto?

  2. ¿Existen otras razones importantes para el uso de los tipos enteros stdint.h?

Según las respuestas que estoy recibiendo, parece que generalmente se prefieren los tipos stdint.h, aunque c convierte uint a int y vuelve. Esto lleva a una pregunta más grande:

  1. Puedo evitar las advertencias del compilador mediante el uso de encasillamiento (por ejemplo, value16 = (uint16_t)(value16 << 8); ). ¿Estoy ocultando el problema? ¿Hay una mejor manera de hacerlo?
pregunta bitsmack

6 respuestas

11

Un compilador que cumpla con los estándares donde int tenía entre 17 y 32 bits puede hacer legítimamente lo que quiera con el siguiente código:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Una implementación que quisiera hacerlo podría generar legítimamente un programa que no haría nada, excepto generar la cadena "Fred" repetidamente en cada pin de puerto usando cada protocolo imaginable. La probabilidad de que un programa se traslade a una implementación que haría tal cosa es excepcionalmente baja, pero es teóricamente posible. Si desea escribir el código anterior para garantizar que no se comprometa con un comportamiento indefinido, sería necesario escribir la última expresión como (uint32_t)x*x o 1u*x*x . En un compilador donde int tiene entre 17 y 31 bits, la última expresión cortaría los bits superiores, pero no se involucraría en un comportamiento indefinido.

Creo que las advertencias de gcc probablemente intentan sugerir que el código escrito no es completamente 100% portátil. Hay momentos en que el código realmente debe escribirse para evitar comportamientos que serían indefinidos en algunas implementaciones, pero en muchos otros casos, simplemente se debe pensar que es poco probable que el código se use en implementaciones que harían cosas demasiado molestas.

Tenga en cuenta que el uso de tipos como int y short puede eliminar algunas advertencias y solucionar algunos problemas, pero es probable que cree otros. La interacción entre tipos como uint16_t y las reglas de promoción de enteros de C es complicada, pero estos tipos probablemente sean mejores que cualquier otra alternativa.

    
respondido por el supercat
6

Dado que el # 2 de Eugene es probablemente el punto más importante, me gustaría agregar que es un aviso en

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

También Jack Ganssle parece ser un partidario de esa regla: enlace

    
respondido por el Tom L.
5

1) Si acaba de convertir desde un entero sin signo a un entero con signo de la misma longitud de ida y vuelta, sin ninguna operación intermedia, obtendrá el mismo resultado cada vez, por lo que no hay problema aquí. Pero varias operaciones lógicas y aritméticas están actuando de manera diferente en los operandos firmados y no firmados.
2) La razón principal para usar los tipos stdint.h es que el tamaño de bits de dichos tipos está definido y es igual en todas las plataformas, lo que no es cierto para int , long etc, así como char no tiene una señal estándar, puede ser firmada o no firmada por defecto. Facilita la manipulación de los datos sabiendo el tamaño exacto sin utilizar comprobaciones y suposiciones adicionales.

    
respondido por el Eugene Sh.
3

Una forma fácil de eliminar las advertencias es evitar el uso de la conversión en GCC. Creo que tienes que habilitar esta opción manualmente, pero si no, puedes usar -Wno-conversion para deshabilitarla. Puede habilitar las advertencias para las conversiones de precisión de signo y FP a través de otras opciones , si aún las desea.

Las advertencias de -Wconversion son casi siempre falsos positivos, por lo que probablemente ni siquiera -Wextra lo habilite de forma predeterminada. Una pregunta de desbordamiento de pila tiene muchas sugerencias para buenos conjuntos de opciones. Basado en mi propia experiencia, este es un buen lugar para comenzar:

  

-std = c99 -pedantic -Wall -Wextra -Wshadow

Agregue más si los necesita, pero es probable que no.

Si debe mantener la conversión -Conversión, puede acortar un poco su código escribiendo solo el operando numérico:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Sin embargo, no es fácil de leer sin el resaltado de sintaxis.

    
respondido por el Adam Haun
2

en cualquier proyecto de software, es muy importante utilizar definiciones de tipo portátiles. (incluso la próxima versión del mismo compilador necesita esta consideración). Un buen ejemplo, hace varios años trabajé en un proyecto donde el compilador actual definía 'int' como 8 bits. La siguiente versión del compilador definió 'int' como 16bits. Como no habíamos usado ninguna definición portátil para 'int', el ram (efectivamente) se duplicó en tamaño y fallaron muchas secuencias de código que dependían de un int de 8 bits. El uso de una definición de tipo portátil habría evitado ese problema (cientos de horas de trabajo para solucionar).

    
respondido por el Richard Williams
1
  1. Sí. Un entero con signo de n bits puede representar aproximadamente la mitad del número de números no negativos como un entero sin signo de n bits, y confiar en las características de desbordamiento es un comportamiento indefinido, por lo que cualquier cosa puede suceder. La gran mayoría de los procesadores actuales y pasados usan dos complementos, por lo que muchas operaciones hacen lo mismo en los tipos integrales firmados y no firmados, pero aun así no todas las operaciones producirán resultados idénticos en cuanto a bits. Más tarde, realmente estás pidiendo problemas adicionales cuando no puedes entender por qué tu código no funciona como se esperaba.

  2. Si bien int y unsigned tienen tamaños definidos para la implementación, a menudo la implementación los elige "inteligentemente" por razones de tamaño o velocidad. En general, me atengo a estos, a menos que tenga una buena razón para hacer lo contrario. Del mismo modo, al considerar si usar int o unsigned generalmente prefiero int a menos que tenga una buena razón para hacerlo de otra manera.

En los casos donde realmente necesito un mejor control sobre el tamaño o la firma de un tipo, por lo general preferiré usar un typedef definido por el sistema (size_t, intmax_t, etc.) o hacer mi propio typedef que indica la función de un tipo dado (prng_int, adc_int, etc.).

    
respondido por el helloworld922

Lea otras preguntas en las etiquetas