Manipulation de bitfield en C

Le problème classique du test et de la définition de bits individuels dans un entier en C est peut-être l’une des compétences de programmation de niveau intermédiaire les plus courantes. Vous définissez et testez avec des masques simples tels que

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

Un article de blog intéressant fait valoir que cela est sujet aux erreurs, difficile à maintenir et à une mauvaise pratique. Le langage C lui-même fournit un access au niveau bit qui est typé et portable:

 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) { [...] } [...] } 

Mais cela me fait reculer .

L’argument intéressant que mon collègue et moi avons eu à ce sujet n’est toujours pas résolu. Les deux styles fonctionnent, et je maintiens que la méthode classique du bitmask est simple, sûre et claire. Mon collègue est d’accord pour dire que c’est commun et facile, mais la méthode de l’union bitfield vaut les quelques lignes supplémentaires pour la rendre portable et plus sûre.

Y a-t-il d’autres arguments pour l’une ou l’autre des parties? En particulier, y a-t-il une défaillance possible, peut-être avec l’endianness, que la méthode du masque de bits risque de manquer, mais où la méthode de la structure est sûre?

Les champs de bits ne sont pas aussi portables que vous le pensez, car “C ne garantit pas le classement des champs dans les mots machine” ( The C book )

Ignorer cela, utilisé correctement , l’une ou l’autre méthode est sûre. Les deux méthodes permettent également un access symbolique aux variables intégrales. Vous pouvez affirmer que la méthode bitfield est plus facile à écrire, mais cela signifie aussi plus de code à revoir.

Si le problème est que la définition et la suppression des bits sont source d’erreurs, alors la bonne chose à faire est d’écrire des fonctions ou des macros pour être sûr de bien le faire.

 // 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)) 

Ce qui rend votre code lisible si cela ne vous dérange pas que val soit une valeur à l'exception de BIT_IS_SET. Si cela ne vous rend pas heureux, alors vous sortez l'affectation, mettez-la entre parenthèses et utilisez-la comme val = SET_BIT (val, someIndex); qui sera équivalent.

En réalité, la solution consiste à découpler ce que vous voulez de ce que vous voulez faire.

Les champs de bits sont excellents et faciles à lire, mais malheureusement, le langage C ne spécifie pas la disposition des champs de bits en mémoire , ce qui signifie qu’ils sont essentiellement inutiles pour traiter des données compressées dans des formats sur disque ou des protocoles filaires. Si vous me demandez, cette décision était une erreur de conception dans C — Ritchie aurait pu choisir une commande et s’y tenir.

Vous devez réfléchir à cela du sharepoint vue d’un écrivain – connaître votre public. Donc, il y a quelques “publics” à considérer.

D’abord, il y a le programmeur C classique, qui a masqué toute sa vie et qui pouvait le faire dans son sumil.

Deuxièmement, il y a le newb, qui n’a aucune idée de ce que c’est tout cela. Ils programmaient php à leur dernier emploi et maintenant ils travaillent pour vous. (Je dis cela comme un newb qui fait php)

Si vous écrivez pour satisfaire le premier public (c’est-à-dire bitmask-all-long), vous les rendrez très heureux et ils pourront garder les yeux bandés. Cependant, le newb devra probablement surmonter une grande courbe d’apprentissage avant de pouvoir maintenir votre code. Ils auront besoin d’apprendre sur les opérateurs binarys, comment vous utilisez ces opérations pour définir / effacer des bits, etc. Vous allez presque certainement avoir des bogues introduits par le newb car il / elle aura toutes les astuces nécessaires pour que cela fonctionne.

En revanche, si vous écrivez pour satisfaire le second public, les utilisateurs auront plus de facilité à maintenir le code. Ils auront plus de facilité à grogner

  flags.force = 0; 

que

  flags &= 0xFFFFFFFE; 

et le premier public deviendra grincheux, mais il est difficile d’imaginer qu’ils ne seraient pas en mesure de maintenir la nouvelle syntaxe. C’est juste beaucoup plus difficile de bousiller. Il n’y aura pas de nouveaux bogues, car le newb conservera plus facilement le code. Vous aurez juste des conférences sur la façon dont “de retour dans ma journée, vous aviez besoin d’une main ferme et d’une aiguille aimantée pour régler les bits … nous n’avons même pas eu de masques bitumés!” (merci XKCD ).

Je vous recommande donc fortement d’utiliser les champs sur les masques de bits pour protéger votre code avec newb.

L’utilisation d’union a un comportement indéfini selon la norme ANSI C et ne devrait donc pas être utilisée (ou du moins ne pas être considérée comme portable).

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

Annexe J – Problèmes de portabilité:

1 Les éléments suivants ne sont pas spécifiés:

– La valeur des octets de remplissage lors du stockage de valeurs dans des structures ou des unions (6.2.6.1).

– la valeur d’un membre d’union autre que le dernier stocké dans (6.2.6.1).

6.2.6.1 – Concepts linguistiques – Représentation des types – Généralités:

6 Lorsqu’une valeur est stockée dans un object de type structure ou union, y compris dans un object membre, les octets de la représentation d’object correspondant à des octets de remplissage prennent des valeurs non spécifiées. [42]) La valeur d’un object structure ou union est jamais une représentation de piège, même si la valeur d’un membre de la structure ou de l’object d’union peut être une représentation de piège.

7 Lorsqu’une valeur est stockée dans un membre d’un object de type union, les octets de la représentation d’object qui ne correspondent pas à ce membre mais correspondent aux autres membres prennent des valeurs non spécifiées.

Donc, si vous voulez conserver la correspondance entre les bits et les entiers, et pour conserver la portabilité, je vous suggère fortement d’utiliser la méthode de masquage, contrairement à la publication de blog liée, ce n’est pas une mauvaise pratique.

De quoi s’agit-il de l’approche bitfield qui vous fait grincer des dents?

Les deux techniques ont leur place, et la seule décision que j’ai est de savoir laquelle utiliser:

Pour un simple bidouillage de bits, j’utilise directement les opérateurs binarys.

Pour tout ce qui est plus complexe – par exemple, les cartes de registre de matériel, l’approche de bitfield gagne à coup sûr.

  • Les bitfields sont plus succincts à utiliser (au désortingment / légèrement / plus de verbosité à écrire.
  • Les champs de bits sont plus robustes (quelle taille est “int”, de toute façon)
  • Les champs de bits sont généralement aussi rapides que les opérateurs binarys.
  • Les champs de bits sont très puissants lorsque vous combinez des champs de bits simples et multiples, et l’extraction du champ à plusieurs bits implique des charges de décalages manuels.
  • Les champs de bits sont effectivement auto-documentés. En définissant la structure et donc en nommant les éléments, je sais ce que cela signifie.
  • Les champs de bits traitent également de manière transparente les structures plus grandes qu’un seul int.
  • Avec les opérateurs au niveau du bit, la pratique (mauvaise) typique est une série de #defines pour les masques de bits.

  • La seule mise en garde avec les champs de bits est de s’assurer que le compilateur a réellement emballé l’object dans la taille souhaitée. Je ne me souviens pas si cela est défini par la norme, donc une assertion (sizeof (myStruct) == N) est une vérification utile.

De toute façon, les champs de bits ont été utilisés dans les logiciels GNU pendant des décennies et cela ne leur a pas fait de mal. Je les aime comme parameters pour les fonctions.

Je dirais que les champs de bits sont conventionnels par opposition aux structures. Tout le monde sait comment AND les valeurs pour désactiver diverses options et le compilateur le résume à des opérations très efficaces sur le processeur.

Si vous utilisez les masques et les tests correctement, les abstractions fournies par le compilateur doivent le rendre robuste, simple, lisible et propre.

Quand j’ai besoin d’un ensemble d’interrupteurs marche / arrêt, je vais continuer à les utiliser en C.

Le billet de blog auquel vous faites référence mentionne raw champ de l’union raw comme méthode d’access alternative pour les champs de bits.

Le but de l’auteur de l’article de blog utilisé raw est correct, mais si vous envisagez de l’utiliser pour autre chose (par exemple, la sérialisation de champs de bits, la définition / vérification de bits individuels), le désastre vous attend. L’ordre des bits en mémoire dépend de l’architecture et les règles de remplissage de la mémoire varient d’un compilateur à l’autre (voir wikipedia ). Ainsi, la position exacte de chaque champ peut être différente.

Cependant, si vous ne prévoyez pas de le mélanger, vous feriez mieux de vous raw débarrasser et vous serez en sécurité.

Eh bien, vous ne pouvez pas vous tromper avec le mappage de structure car les deux champs sont accessibles, ils peuvent être utilisés de manière interchangeable.

Un des avantages des champs de bits est que vous pouvez facilement agréger les options:

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

Dans certains environnements, par exemple en ce qui concerne les options de protocole, il peut être assez ancien de définir individuellement des options ou d’utiliser plusieurs parameters pour transférer les états intermédiaires afin d’obtenir un résultat final.

Mais parfois, définir flag.blah et avoir la liste déroulante dans votre IDE est idéal, surtout si vous êtes comme moi et que vous ne vous souvenez pas du nom du drapeau que vous voulez définir sans faire constamment référence à la liste.

Personnellement, je crains parfois de déclarer des types booléens parce que, à un moment donné, je finirai par avoir l’impression erronée que le champ que je viens de modifier n’était pas dépendant (pensez à la concurrence multi-thread) sur le statut r / w des autres champs non liés qui arrivent à partager le même mot de 32 bits.

Mon vote est que cela dépend du contexte de la situation et dans certains cas, les deux approches peuvent très bien fonctionner.

En C ++, utilisez simplement std::bitset .

C’est sujet aux erreurs, oui. J’ai vu beaucoup d’erreurs dans ce type de code, principalement parce que certaines personnes ont le sentiment de devoir s’en mêler et la logique métier de manière totalement désorganisée, créant des cauchemars de maintenance. Ils pensent que les “vrais” programmeurs peuvent écrire la value |= mask; , value &= ~mask; ou pire encore à n’importe quel endroit, et ça va. Mieux encore, s’il y a un opérateur d’incrémentation, quelques cas de memcpy , de pointeurs et quelle que soit la syntaxe obscure et sujette à erreur qui leur vienne à l’esprit à ce moment-là. Bien sûr, il n’est pas nécessaire d’être cohérent et vous pouvez inverser les bits de deux ou trois manières différentes, dissortingbuées aléatoirement.

Mon conseil serait:

  1. Encapsulez ceci dans une classe, avec des méthodes telles que SetBit(...) et ClearBit(...) . (Si vous n’avez pas de classes en C, dans un module). Pendant que vous y êtes, vous pouvez documenter tous leurs comportements.
  2. Unité teste cette classe ou ce module.

Votre première méthode est préférable, à mon humble avis. Pourquoi masquer le problème? Le bidouillage est une chose très simple. C l’a bien fait. Endianess n’a pas d’importance. La solution syndicale ne fait que nommer les choses. 11 pourrait être mystérieux, mais #défini à un nom significatif ou énuméré devrait suffire.

Les programmeurs qui ne peuvent pas gérer les fondamentaux comme “| & ^ ~” sont probablement dans la mauvaise ligne de travail.

Quand je google pour “c operators”

Les trois premières pages sont les suivantes:

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

..si je pense que l’argument sur les gens nouveaux dans la langue est un peu idiot.

J’utilise presque toujours les opérations logiques avec un masque de bits, directement ou en tant que macro. par exemple

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

accessoirement, votre définition d’union dans la question initiale ne fonctionnerait pas sur ma combinaison processeur / compilateur. Le type int ne fait que 16 bits de large et les définitions de bitfield sont 32. Pour le rendre légèrement plus portable, vous devrez définir un nouveau type 32 bits que vous pourrez ensuite mapper sur le type de base requirejs sur chaque architecture cible. exercice de portage. Dans mon cas

 typedef unsigned long int uint32_t 

et dans l’exemple 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; 

Le int incrusté doit également être rendu non signé.

Eh bien, je suppose que c’est une façon de le faire, mais je préférerais toujours que ce soit simple .

Une fois que vous êtes habitué, utiliser des masques est simple, sans ambiguïté et portable.

Les champs de bits sont simples, mais ils ne sont pas portables sans avoir à faire de travail supplémentaire.

Si vous devez écrire un code conforme à la norme MISRA , les directives de MISRA désapprouvent les champs de bits, les unions et de nombreux autres aspects de C, afin d’éviter tout comportement indéfini ou dépendant de l’implémentation.

En général, celui qui est plus facile à lire et à comprendre est aussi celui qui est plus facile à maintenir. Si vous avez des collègues qui sont nouveaux dans C, l’approche «plus sûre» sera probablement la plus facile à comprendre pour eux.

Les champs de bits sont excellents, sauf que les opérations de manipulation de bits ne sont pas atomiques et peuvent donc entraîner des problèmes dans les applications multithread.

Par exemple, on pourrait supposer qu’une macro:

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

Définit une opération atomique, puisque | = est une instruction. Mais le code ordinaire généré par un compilateur n'essaiera pas de rendre | = atomique.

Ainsi, si plusieurs threads exécutent différentes opérations sur les bits de réglage, l'une des opérations sur les bits définies peut être fausse. Puisque les deux threads seront exécutés:

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

Le résultat peut être le champ '= champ OU masque1 OU masque2 (intentionnel), ou le résultat peut être le champ' = champ OU masque1 (non intentionnel) ou le résultat peut être champ '= champ OU masque2 (non prévu).