Contexto
Estamos transfiriendo código C que se compiló originalmente usando un compilador C de 8 bits para el microcontrolador PIC. Un modismo común que se usó para evitar que las variables globales sin firmar (por ejemplo, contadores de errores) vuelvan a cero es el siguiente:
if(~counter) counter++; El operador bit a bit aquí invierte todos los bits y la declaración solo es verdadera si el counter es menor que el valor máximo. Es importante destacar que esto funciona independientemente del tamaño de la variable.
Problema
Ahora apuntamos a un procesador ARM de 32 bits usando GCC. Hemos notado que el mismo código produce resultados diferentes. Por lo que podemos decir, parece que la operación de complemento bit a bit devuelve un valor que tiene un tamaño diferente al que esperaríamos. Para reproducir esto, compilamos, en GCC:
uint8_t i = 0; int sz; sz = sizeof(i); printf("Size of variable: %d\n", sz); // Size of variable: 1 sz = sizeof(~i); printf("Size of result: %d\n", sz); // Size of result: 4 En la primera línea de salida, obtenemos lo que esperaríamos: i es 1 byte. Sin embargo, el complemento bit a bit de i es en realidad cuatro bytes , lo que causa un problema porque las comparaciones con esto ahora no darán los resultados esperados. Por ejemplo, si está haciendo (donde i es un uint8_t correctamente inicializado):
if(~i) i++; veremos i "cambio" de 0xFF a 0x00. Este comportamiento es diferente en GCC en comparación con cuando solía funcionar como pretendíamos en el compilador anterior y el microcontrolador PIC de 8 bits.
Somos conscientes de que podemos resolver esto emitiendo así:
if((uint8_t)~i) i++;o por
if(i < 0xFF) i++; sin embargo, en ambas soluciones, el tamaño de la variable debe conocerse y es propenso a errores para el desarrollador de software. Este tipo de comprobaciones de límites superiores se producen en todo el código base. Hay varios tamaños de variables (p. ej., uint16_t y unsigned char , etc.) y cambiarlos en un código base que funciona de otro modo no es algo que estemos esperando.
Pregunta
¿Es correcta nuestra comprensión del problema? ¿Hay opciones disponibles para resolverlo que no requieran volver a visitar cada caso en el que hayamos usado este modismo? ¿Es correcta nuestra suposición de que una operación como el complemento bit a bit debería devolver un resultado del mismo tamaño que el operando? Parece que esto se rompería, dependiendo de las arquitecturas del procesador. Siento que estoy tomando pastillas locas y que C debería ser un poco más portátil que esto. Una vez más, nuestra comprensión de esto podría estar equivocada.
En la superficie, esto puede no parecer un gran problema, pero este idioma que funcionaba anteriormente se usa en cientos de ubicaciones y estamos ansiosos por comprender esto antes de proceder con cambios costosos.
Nota: aquí hay una pregunta duplicada aparentemente similar pero no exacta: la operación bit a bit en char da un resultado de 32 bits
No vi el quid real del problema discutido allí, es decir, el tamaño del resultado de un complemento bit a bit es diferente de lo que se pasa al operador.
en tamaño de (i); solicita el tamaño de la variable i , por lo que 1
en tamaño de(~i); solicitas el tamaño del tipo de la expresión, que es un int , en tu caso 4
Usar
si (~ yo)
para saber si no valoro 255 (en tu caso con un uint8_t ) no es muy legible, solo hazlo
if (i != 255)y tendrás un código portátil y legible
Hay varios tamaños de variables (p. ej., uint16_t y char sin firmar, etc.)
Para administrar cualquier tamaño de unsigned :
if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))La expresión es constante, por lo que se calcula en tiempo de compilación.
#include <limits.h> para CHAR_BIT e #include <stdint.h> para uintmax_t
Lo que está viendo es el resultado de promociones de enteros . En la mayoría de los casos en los que se usa un valor entero en una expresión, si el tipo del valor es menor que int , el valor se promueve a int . Esto está documentado en la sección 6.3.1.1p2 del estándar C :
Lo siguiente se puede usar en una expresión siempre que se pueda usar un
into un intunsigned int
- Un objeto o expresión con un tipo de entero (distinto de
into intunsigned int) cuyo rango de conversión de entero es menor o igual que el rango deintyunsigned int.- Un campo de bits de tipo
_Bool,int ,firmado, orint sin firmar`.Si un
intpuede representar todos los valores del tipo original (restringido por el ancho, para un campo de bits), el valor se convierte en unint; de lo contrario, se convierte en ununsigned int. Éstos se llaman las promociones enteras . Todos los demás tipos no se modifican por las promociones de enteros.
Entonces, si una variable tiene el tipo uint8_t y el valor 255, usar cualquier operador que no sea una conversión o asignación en ella primero la convertirá al tipo int con el valor 255 antes de realizar la operación. Es por eso que sizeof(~i) te da 4 en lugar de 1.
La sección 6.5.3.3 describe que las promociones de enteros se aplican al operador ~ :
El resultado del operador
~es el complemento bit a bit de su operando (promovido) (es decir, cada bit en el resultado se establece si y solo si el bit correspondiente en el operando convertido no está establecido). Las promociones de enteros se realizan en el operando y el resultado tiene el tipo promovido. Si el tipo promocionado es un tipo sin firmar, la expresión~Ees equivalente al valor máximo representable en ese tipo menosE.
Entonces, suponiendo un int de 32 bits, si el counter tiene el valor de 8 bits 0xff , se convierte al valor de 32 bits 0x000000ff , y al aplicarle ~ , obtiene 0xffffff00 .
Probablemente la forma más sencilla de manejar esto es sin tener que saber el tipo para verificar si el valor es 0 después de incrementarlo y, de ser así, disminuirlo.
if (!++counter) counter--;El ajuste de enteros sin signo funciona en ambas direcciones, por lo que disminuir un valor de 0 le da el mayor valor positivo.
6.5.3.3 Operadores aritméticos unarios
...
4 El resultado del operador~es el complemento bit a bit de su operando (promovido) (es decir, cada bit en el resultado se establece si y solo si el bit correspondiente en el operando convertido no está establecido). Las promociones de enteros se realizan en el operando y el resultado tiene el tipo promovido . Si el tipo promocionado es un tipo sin firmar, la expresión~Ees equivalente al valor máximo representable en ese tipo menosE.
El problema es que el operando de ~ se promociona a int antes de que se aplique el operador.
Desafortunadamente, no creo que haya una manera fácil de salir de esto. Escribiendo
if ( counter + 1 ) counter++;no ayudará porque las promociones también se aplican allí. Lo único que puedo sugerir es crear algunas constantes simbólicas para el valor máximo que desea que represente ese objeto y probar contra eso:
#define MAX_COUNTER 255 ... if ( counter < MAX_COUNTER-1 ) counter++;Antes de stdint.h, los tamaños de las variables pueden variar de un compilador a otro y los tipos de variables reales en C siguen siendo int, long, etc. y aún están definidos por el autor del compilador en cuanto a su tamaño. No se trata de supuestos específicos estándar ni objetivos. Luego, el autor (o los autores) deben crear stdint.h para mapear los dos mundos, ese es el propósito de stdint.h para mapear uint_this that a int, long, short.
Si está transfiriendo código de otro compilador y usa char, short, int, long, entonces tiene que pasar por cada tipo y hacer el port usted mismo, no hay forma de evitarlo. Y o terminas con el tamaño correcto para la variable, la declaración cambia pero el código tal como está escrito funciona...
if(~counter) counter++;o... suministrar la máscara o encasillar directamente
if((~counter)&0xFF) counter++; if((uint_8)(~counter)) counter++;Al final del día, si desea que este código funcione, debe migrarlo a la nueva plataforma. Su elección en cuanto a cómo. Sí, tienes que dedicar tiempo a resolver cada caso y hacerlo bien, de lo contrario, seguirás volviendo a este código, que es aún más costoso.
Si aísla los tipos de variables en el código antes de portar y qué tamaño tienen los tipos de variables, aísle las variables que hacen esto (debería ser fácil de grep) y cambie sus declaraciones usando definiciones stdint.h que, con suerte, no cambiarán en el futuro, y se sorprendería, pero a veces se usan encabezados incorrectos, así que incluso ponga cheques para que pueda dormir mejor por la noche
if(sizeof(uint_8)!=1) return(FAIL);Y aunque ese estilo de codificación funciona (if(~counter) counter++;), para los deseos de portabilidad ahora y en el futuro, es mejor usar una máscara para limitar específicamente el tamaño (y no confiar en la declaración), haz esto cuando el el código está escrito en primer lugar o simplemente finaliza la portabilidad y luego no tendrá que volver a portarla otro día. O para hacer que el código sea más legible, haga if x<0xFF entonces o x!=0xFF o algo así, entonces el compilador puede optimizarlo en el mismo código que lo haría para cualquiera de estas soluciones, simplemente lo hace más legible y menos riesgoso ...
Depende de la importancia del producto o de cuántas veces desee enviar parches/actualizaciones o hacer rodar un camión o caminar hasta el laboratorio para arreglar el problema, ya sea que trate de encontrar una solución rápida o simplemente toque las líneas de código afectadas. si son solo cien o pocos, no es un puerto tan grande.
Aquí hay varias opciones para implementar "Agregar 1 a x pero sujetar al valor máximo representable", dado que x es un tipo de número entero sin signo:
Agregue uno si y solo si x es menor que el valor máximo representable en su tipo:
x += x < Maximum(x); Consulte el siguiente elemento para conocer la definición de Maximum . Este método tiene una buena posibilidad de ser optimizado por un compilador para instrucciones eficientes como una comparación, alguna forma de conjunto o movimiento condicional y una adición.
Compare con el valor más grande del tipo:
if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x (Esto calcula 2 N , donde N es el número de bits en x , desplazando 2 por N −1 bits. Hacemos esto en lugar de desplazar 1 N bits porque un desplazamiento por el número de bits en un tipo no está definido por el estándar de C. La macro CHAR_BIT puede ser desconocida para algunos, es la cantidad de bits en un byte, por lo que sizeof x * CHAR_BIT es la cantidad de bits en el tipo de x ).
Esto se puede envolver en una macro según se desee por estética y claridad:
#define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1) if (x < Maximum(x)) ++x; Incremente x y corrija si llega a cero, usando un if :
if (!++x) --x; // !++x is true if ++x wraps to zero. Incremente x y corrija si llega a cero, usando una expresión:
++x; x -= !x;Esto es nominalmente sin ramas (a veces beneficioso para el rendimiento), pero un compilador puede implementarlo igual que el anterior, usando una rama si es necesario, pero posiblemente con instrucciones incondicionales si la arquitectura de destino tiene instrucciones adecuadas.
Una opción sin sucursales, usando la macro anterior, es:
x += 1 - x/Maximum(x); Si x es el máximo de su tipo, esto se evalúa como x += 1-1 . De lo contrario, es x += 1-0 . Sin embargo, la división es algo lenta en muchas arquitecturas. Un compilador puede optimizar esto a instrucciones sin división, según el compilador y la arquitectura de destino.