Manipulación de Bitfield en C

El problema clásico de probar y configurar bits individuales en un entero en C es quizás una de las habilidades de progtwigción de nivel intermedio más comunes. Establece y prueba con máscaras de bits simples como

unsigned int mask = 1<<11; if (value & mask) {....} // Test for the bit value |= mask; // set the bit value &= ~mask; // clear the bit 

Una publicación de blog interesante argumenta que esto es propenso a errores, difícil de mantener y una mala práctica. El lenguaje C en sí proporciona acceso a nivel de bits que es seguro y portátil:

 typedef unsigned int boolean_t; #define FALSE 0 #define TRUE !FALSE typedef union { struct { boolean_t user:1; boolean_t zero:1; boolean_t force:1; int :28; /* unused */ boolean_t compat:1; /* bit 31 */ }; int raw; } flags_t; int create_object(flags_t flags) { boolean_t is_compat = flags.compat; if (is_compat) flags.force = FALSE; if (flags.force) { [...] } [...] } 

Pero esto me hace temblar .

El argumento interesante que mi compañero de trabajo y yo teníamos sobre esto aún no está resuelto. Ambos estilos funcionan, y mantengo que el método clásico de máscara de bits es fácil, seguro y claro. Mi compañero de trabajo está de acuerdo en que es común y fácil, pero el método de unión de campo de bitácora vale las pocas líneas adicionales para que sea portátil y más seguro.

¿Hay más argumentos para cualquiera de los lados? En particular, ¿hay alguna falla posible, tal vez con endianness, que el método de la máscara de bits se puede perder, pero donde el método de la estructura es seguro?

Los campos de bits no son tan portátiles como crees, ya que “C no garantiza el orden de los campos dentro de las palabras de la máquina” ( El libro C )

Ignorando que, usado correctamente , cualquiera de los métodos es seguro. Ambos métodos también permiten el acceso simbólico a variables integrales. Puede argumentar que el método de campo de bits es más fácil de escribir, pero también significa más código para revisar.

Si el problema es que establecer y borrar bits es propenso a errores, entonces lo correcto es escribir funciones o macros para asegurarse de hacerlo bien.

 // off the top of my head #define SET_BIT(val, bitIndex) val |= (1 < < bitIndex) #define CLEAR_BIT(val, bitIndex) val &= ~(1 << bitIndex) #define TOGGLE_BIT(val, bitIndex) val ^= (1 << bitIndex) #define BIT_IS_SET(val, bitIndex) (val & (1 << bitIndex)) 

Lo que hace que tu código sea legible si no te importa que val tenga que ser un lvalue a excepción de BIT_IS_SET. Si eso no te hace feliz, entonces sacas la asignación, la entrelazas y la usas como val = SET_BIT (val, someIndex); que será equivalente.

Realmente, la respuesta es considerar desacoplar lo que quieres de cómo quieres hacerlo.

Los campos de bits son geniales y fáciles de leer, pero desafortunadamente el lenguaje C no especifica el diseño de los campos de bits en la memoria , lo que significa que son esencialmente inútiles para manejar los datos empaquetados en formatos en disco o protocolos de cable binarios. Si me preguntas, esta decisión fue un error de diseño en C-Ritchie podría haber elegido una orden y atascado con ella.

Tienes que pensar en esto desde la perspectiva de un escritor: conoce a tu público. Entonces hay un par de “audiencias” para considerar.

Primero está el clásico progtwigdor en C, que se ha enmascarado toda la vida y podría hacerlo mientras duerme.

En segundo lugar está el newb, que no tiene idea de qué es todo esto | y cosas así. Estaban progtwigndo php en su último trabajo y ahora trabajan para usted. (Digo esto como un newb que hace php)

Si escribe para satisfacer a la primera audiencia (es decir, máscara de bits durante todo el día), los hará muy felices y podrán mantener el código con los ojos vendados. Sin embargo, es probable que el newb necesite superar una gran curva de aprendizaje antes de poder mantener su código. Necesitarán aprender sobre operadores binarios, cómo usar estas operaciones para establecer / borrar bits, etc. Seguramente va a tener errores introducidos por el newb ya que él / ella tiene todos los trucos necesarios para que esto funcione.

Por otro lado, si escribe para satisfacer a la segunda audiencia, a los newbs les será más fácil mantener el código. Tendrán un tiempo más fácil groking

  flags.force = 0; 

que

  flags &= 0xFFFFFFFE; 

y la primera audiencia se pondrá gruñona, pero es difícil imaginar que no puedan asimilar y mantener la nueva syntax. Es mucho más difícil arruinarlo. No habrá nuevos errores, porque el newb mantendrá más fácilmente el código. Tendrás conferencias sobre cómo “en mi época necesitabas una mano firme y una aguja magnetizada para fijar los bits … ¡Ni siquiera TENEMOS máscaras de bits!” (gracias XKCD ).

Por lo tanto, recomendaría encarecidamente utilizar los campos sobre las máscaras de bits para newb-safe your code.

El uso de la unión tiene un comportamiento indefinido de acuerdo con el estándar ANSI C, y por lo tanto, no se debe utilizar (o al menos no se debe considerar portátil).

De la norma ISO / IEC 9899: 1999 (C99) :

Anexo J – Cuestiones de portabilidad:

1 Los siguientes son no especificados:

– El valor de los bytes de relleno cuando se almacenan valores en estructuras o uniones (6.2.6.1).

– El valor de un miembro de la unión que no sea el último almacenado en (6.2.6.1).

6.2.6.1 – Conceptos de lenguaje – Representación de tipos – General:

6 Cuando se almacena un valor en un objeto de estructura o tipo de unión, incluido en un objeto miembro, los bytes de la representación de objeto que corresponden a cualquier byte de relleno toman valores no especificados. [42]) El valor de una estructura o de un objeto de unión es nunca una representación de trampa, aunque el valor de un miembro de la estructura o del objeto de unión puede ser una representación de trampa.

7 Cuando se almacena un valor en un miembro de un objeto de tipo unión, los bytes de la representación del objeto que no corresponden a ese miembro pero que corresponden a otros miembros toman valores no especificados.

Por lo tanto, si desea conservar la correspondencia de bitfield ↔ entero y mantener la portabilidad, le sugiero encarecidamente que utilice el método de enmascaramiento de bits, que al contrario de la publicación de blog vinculada, no es una mala práctica.

¿De qué se trata el enfoque del campo de bits que te hace temblar?

Ambas técnicas tienen su lugar, y la única decisión que tengo es cuál usar:

Para el simple toque de bits de “una sola vez”, utilizo los operadores bit a bit directamente.

Para cualquier cosa más compleja, por ejemplo, mapas de registros de hardware, el enfoque del campo de bits gana indiscutiblemente.

  • Los bitfields son más sucintos de usar (a expensas de / ligeramente / más de verbosidad para escribir).
  • Los bitfields son más robustos (de todos modos, el tamaño es “int”)
  • Los campos de bits suelen ser tan rápidos como los operadores bit a bit.
  • Los campos de bits son muy potentes cuando tiene una combinación de campos de bits únicos y múltiples, y la extracción del campo de bits múltiples implica una gran cantidad de cambios manuales.
  • Los Bitfields son efectivamente autodocumentados. Al definir la estructura y, por lo tanto, nombrar los elementos, sé lo que se supone que debe hacer.
  • Bitfields también maneja sin problemas estructuras más grandes que una sola int.
  • Con los operadores bit a bit, la práctica típica (mala) es una gran cantidad de #defines para las máscaras de bits.

  • La única advertencia con bitfields es asegurarse de que el comstackdor realmente haya empaquetado el objeto en el tamaño que deseaba. No recuerdo si esto está definido por el estándar, por lo que una afirmación (sizeof (myStruct) == N) es una verificación útil.

De cualquier manera, bitfields se han utilizado en el software GNU durante décadas y no les ha hecho ningún daño. Me gustan como parámetros para las funciones.

Yo diría que los bitfields son convencionales en oposición a las estructuras. Todos saben cómo Y los valores para desactivar varias opciones y el comstackdor lo reduce a operaciones bit a bit muy eficientes en la CPU.

Siempre que utilice las máscaras y las pruebas de la manera correcta, las abstracciones que proporciona el comstackdor deben ser robustas, simples, legibles y limpias.

Cuando necesito un conjunto de interruptores de encendido / apagado, voy a seguir usándolos en C.

La publicación del blog a la que se refiere menciona raw campo de unión raw formato como método de acceso alternativo para los campos de bits.

Los fines que utiliza el autor de la publicación de blog en raw están bien; sin embargo, si planea usarlo para cualquier otra cosa (por ejemplo, serialización de campos de bits, configuración / comprobación de bits individuales), el desastre lo está esperando a la vuelta de la esquina. El orden de los bits en la memoria depende de la architecture y las reglas de memoria varían desde comstackdor hasta comstackdor (ver wikipedia ), por lo que la posición exacta de cada bitfield puede diferir; en otras palabras, nunca se puede saber a qué bit de raw corresponde cada bitfield.

Sin embargo, si no planeas mezclarlo, es mejor que lo raw y estarás a salvo.

Bueno, no puedes equivocarte con el mapeo de estructuras ya que ambos campos son accesibles y pueden usarse de forma intercambiable.

Un beneficio para los campos de bit es que puede agregar opciones fácilmente:

 mask = USER|FORCE|ZERO|COMPAT; vs flags.user = true; flags.force = true; flags.zero = true; flags.compat = true; 

En algunos entornos, como el tratamiento de las opciones de protocolo, puede ser bastante antiguo tener que establecer opciones individualmente o utilizar múltiples parámetros para transportar estados intermedios para lograr un resultado final.

Pero a veces establecer flag.blah y tener la lista emergente en su IDE es genial, especialmente si le gusto y no puede recordar el nombre de la bandera que desea establecer sin hacer referencia constante a la lista.

Personalmente, algunas veces rehuiré declarar tipos booleanos porque en algún momento terminaré con la impresión errónea de que el campo que acabo de alternar no era dependiente (piense en la concurrencia de múltiples hilos) en el estado de “otro” aparentemente. campos no relacionados que comparten la misma palabra de 32 bits.

Mi voto es que depende del contexto de la situación y, en algunos casos, ambos enfoques pueden funcionar muy bien.

En C ++, simplemente use std::bitset .

Es propenso a errores, sí. He visto muchos errores en este tipo de código, principalmente porque algunas personas sienten que deben meterse con él y la lógica de negocios de una manera totalmente desorganizada, creando pesadillas de mantenimiento. Creen que los progtwigdores “reales” pueden escribir value |= mask; , value &= ~mask; o incluso cosas peores en cualquier lugar, y eso está bien. Incluso mejor si hay algún operador de incremento, un par de memcpy ‘s, punteros y cualquier syntax obscura y propensa a errores que se les ocurra en ese momento. Por supuesto, no es necesario ser coherente y puede voltear bits de dos o tres maneras diferentes, distribuidos aleatoriamente.

Mi consejo sería:

  1. Encapsule esto —- en una clase, con métodos como SetBit(...) y ClearBit(...) . (Si no tiene clases en C, en un módulo). Mientras lo hace, puede documentar todo su comportamiento.
  2. Unidad de prueba esa clase o módulo.

Su primer método es preferible, en mi humilde opinión. ¿Por qué ofuscar el problema? El toque de bits es algo realmente básico. C lo hizo bien. Endianess no importa. Lo único que hace la solución sindical es nombrar cosas. 11 podría ser misterioso, pero #definido a un nombre significativo o enumizado debería ser suficiente.

Los progtwigdores que no pueden manejar los fundamentos como “| & ^ ~” probablemente estén en la línea de trabajo incorrecta.

Cuando busco “operadores c”

Las primeras tres páginas son:

http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/V40F_HTML/AQTLTBTE/DOCU_059.HTM http: //www.cs. mun.ca/~michael/c/op.html

..so creo que la discusión sobre gente nueva en el lenguaje es un poco tonta.

Casi siempre uso las operaciones lógicas con una máscara de bits, ya sea directamente o como una macro. p.ej

  #define ASSERT_GPS_RESET() { P1OUT &= ~GPS_RESET ; } 

Incidentalmente, la definición de su unión en la pregunta original no funcionaría en mi combinación de procesador / comstackdor. El tipo int tiene solo 16 bits de ancho y las definiciones de campo de bits son 32. Para que sea un poco más portátil, debería definir un nuevo tipo de 32 bits que luego podría asignar al tipo de base requerido en cada architecture de destino como parte del ejercicio de portabilidad En mi caso

 typedef unsigned long int uint32_t 

y en el ejemplo original

 typedef unsigned int uint32_t typedef union { struct { boolean_t user:1; boolean_t zero:1; boolean_t force:1; int :28; /* unused */ boolean_t compat:1; /* bit 31 */ }; uint32_t raw; } flags_t; 

El int superpuesto también debe estar sin signo.

Bueno, supongo que esa es una forma de hacerlo, pero siempre preferiría mantenerlo simple .

Una vez que esté acostumbrado, el uso de máscaras es sencillo, inequívoco y portátil.

Los campos de bits son sencillos, pero no son portátiles sin tener que hacer un trabajo adicional.

Si alguna vez tiene que escribir el código compatible con MISRA , las pautas MISRA fruncen el ceño en los campos de bits, las uniones y muchos otros aspectos de C, para evitar un comportamiento indefinido o dependiente de la implementación.

En general, el que es más fácil de leer y comprender es el que también es más fácil de mantener. Si tiene compañeros de trabajo que son nuevos en C, el enfoque “más seguro” será probablemente el más fácil de entender para ellos.

Los campos de bits son geniales, excepto que las operaciones de manipulación de bits no son atómicas y, por lo tanto, pueden provocar problemas en la aplicación de subprocesos múltiples.

Por ejemplo, uno podría suponer que una macro:

 #define SET_BIT(val, bitIndex) val |= (1 < < bitIndex) 

Define una operación atómica, ya que | = es una statement. Pero el código ordinario generado por un comstackdor no intentará hacer | = atómico.

Entonces, si múltiples hilos ejecutan diferentes operaciones de bit configuradas, una de las operaciones de bit configuradas podría ser espuria. Dado que ambos hilos se ejecutarán:

  thread 1 thread 2 LOAD field LOAD field OR mask1 OR mask2 STORE field STORE field 

El resultado puede ser campo '= campo O máscara1 O máscara2 (intentado), o el resultado puede ser campo' = campo O máscara1 (no intentado) o el resultado puede ser campo '= campo O máscara2 (no previsto).