Manipulation de Bitfield en C
Le problème classique de tester et de définir des 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 de bits 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 soutient que c'est sujet aux erreurs, difficile à maintenir et mauvaise pratique. Le langage C lui-même fournit un accès au niveau des bits qui est typesafe 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 grincer des dents.
L'intéressant argument mon collègue et moi avons eu à ce sujet est toujours non résolu. Les deux styles fonctionnent, et je maintiens que la méthode bitmask classique est facile, sûre et claire. Mon collègue convient que c'est commun et facile, mais la méthode bitfield union vaut les quelques lignes supplémentaires pour la rendre portable et plus sûre.
Y a-t-il d'autres arguments pour les deux parties? En particulier, y a-t-il un échec possible, peut-être avec endianness, que la méthode bitmask peut manquer mais où la méthode de structure est sûre?
17 réponses
Les champs de bits ne sont pas aussi portables que vous le pensez, car "C NE DONNE AUCUNE GARANTIE de l'ordre des champs dans les mots de la machine" ( Le Livre C )
En ignorant cela, utilisé correctement , l'une ou l'autre méthode est sûre. Les deux méthodes permettent également un accès symbolique aux variables intégrales. Vous pouvez soutenir que la méthode bitfield est plus facile à écrire, mais cela signifie aussi plus de code à examiner.
Si le problème est que la définition et la suppression des bits sont sujettes aux erreurs, alors la bonne chose à faire est d'écrire des fonctions ou des macros pour vous assurer de le faire correctement.
// 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 doit être une lvalue sauf pour BIT_IS_SET. Si cela ne vous rend pas heureux, alors vous sortez l'affectation, la mettez entre parenthèses et l'utilisez comme val = SET_BIT( val, someIndex); qui sera équivalent.
Vraiment, la réponse est d'envisager de découpler le ce que vous vous voulez de la façon dont vous voulez le faire.
Les champs de bits sont géniaux 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 emballées dans des formats sur disque ou des protocoles binaires. Si vous me demandez, cette décision était une erreur de conception dans C-Ritchie aurait pu choisir une commande et coincé avec elle.
Vous devez penser à cela du point de vue d'un écrivain-connaître votre public. Il y a donc quelques "publics" à considérer.
D'abord, il y a le programmeur C classique, qui a bitmasked toute leur vie et pourrait le faire dans leur sommeil.
Deuxièmement, il y a le newb, qui n'a aucune idée de ce qu'est tout cela |, & stuff. Ils programmaient php à leur dernier travail et maintenant ils travaillent pour vous. (Je dis cela comme un newb qui fait php)
Si vous écrivez pour satisfaire le premier public (qui est bitmask-toute la journée), vous les rendrez très heureux, et ils seront en mesure de maintenir le code les yeux bandés. Cependant, les newb devront probablement surmonter une grande courbe d'apprentissage avant de pouvoir maintenir votre code. Ils devront en apprendre davantage sur les opérateurs binaires, comment vous utilisez ces opérations pour définir / effacer des bits,etc. Vous allez certainement avoir des bugs introduits par le newb car il / elle toutes les astuces nécessaires pour que cela fonctionne.
D'autre part, si vous Ecrire pour satisfaire le deuxième public, les newbs auront un temps plus facile à maintenir le code. Ils auront un temps plus facile groking
flags.force = 0;
Que
flags &= 0xFFFFFFFE;
Et le premier public va juste devenir grincheux, mais il est difficile d'imaginer qu'ils ne seraient pas en mesure de grok et de maintenir la nouvelle syntaxe. C'est juste beaucoup plus difficile à visser. Il n'y aura pas de nouveaux bugs, car le newb maintiendra plus facilement le code. Vous aurez juste des conférences sur la façon dont "de retour dans mon jour, vous aviez besoin d'une main ferme et un aiguille magnétisée pour régler les bits... nous n'avions même pas de masques de bits!"(merci XKCD).
Je recommande donc fortement d'utiliser les champs sur les masques de bits pour sécuriser votre code.
L'utilisation de l'union a un comportement indéfini selon la norme ANSI C, et ne doit 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-Questions 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 de l'union autre que le dernier stocké dans (6.2.6.1).
6.2.6.1-Concepts linguistiques-représentation des Types-Général:
6 lorsqu'une valeur est stockée dans un objet de type structure ou union, y compris dans un membre objet, les octets de la représentation d'objet qui correspondent à tous les octets de remplissage prennent valeurs non spécifiées.[42]) la valeur d'un objet de structure ou d'union n'est jamais un piège représentation, même si la valeur d'un membre de la structure ou de l'objet union peut être une représentation de piège.
7 Lorsqu'une valeur est stockée dans un membre d'un objet de type union, les octets de l'objet représentation qui ne correspond pas à ce membre mais qui correspond à d'autres membres prenez des valeurs non spécifiées.
Donc, si vous voulez garder la correspondance integer de bitfield↔, et garder la portabilité, je vous suggère fortement d'utiliser la méthode bitmasking, que contrairement à l'article de blog lié, il est Pas mauvaise pratique.
Qu'est-ce que c'est à propos 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 celle à utiliser:
Pour un simple tripotage de bits "unique", j'utilise directement les opérateurs au niveau du BIT.
Pour tout ce qui est plus complexe - par exemple les cartes de registre matériel, l'approche bitfield gagne haut la main.
- les champs de bits sont plus succincts à utiliser (au détriment de / légèrement / plus le niveau de verbosité à écrire.
- Bitfields sont plus robuste (quelle taille est " int", de toute façon)
- les champs de bits sont généralement aussi vite que les opérateurs bit à bit.
- les champs de bits sont très puissants lorsque vous avoir un mélange de bit unique et multiple les champs, et l'extraction de la champ de bits multiples implique des charges de manuel de quarts de travail.
- Bitfields sont auto-documentation efficace. Par définir la structure et donc nommer les éléments, je sais ce que c'est censé faire.
- Bitfields gère également de manière transparente des structures plus grandes qu'une seule int.
Avec les opérateurs binaires, la pratique typique (Mauvaise) est une série de #defines pour les masques de bits.
Le seul inconvénient avec bitfields est de s'assurer que le compilateur a vraiment emballé l'objet dans la taille que vous vouliez. Je ne me souviens pas si cela est défini par la norme, donc un assert (sizeof (myStruct) = = N) est une vérification utile.
De toute façon, bitfields ont été utilisés dans les logiciels GNU depuis des décennies et cela ne leur a pas fait de mal. Je les aime comme paramètres aux fonctions.
Je dirais que les champs de bits sont conventionnels par opposition aux structures. Tout le monde sait comment et les valeurs pour définir diverses options off et le compilateur se résume à très efficaces opérations bit à bit sur le CPU.
Si vous utilisez les masques et les tests de la bonne manière, les abstractions fournies par le compilateur doit 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.
L'article de blog auquel vous faites référence mentionne le champ union raw
comme méthode d'accès alternative pour les champs bitfields.
Les fins pour lesquelles l'auteur du billet de blog a utilisé raw
sont correctes, mais si vous prévoyez de l'utiliser pour autre chose (par exemple, sérialisation des champs de bits, définition/vérification de bits individuels), la catastrophe vous attend au coin de la rue. 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 ), donc, la position exacte de chaque bitfield peut différer, en d'autres termes, vous ne pouvez jamais être sûr de quel bit de raw
chaque bitfield correspond.
Toutefois, si vous ne prévoyez pas de le mélanger à mieux vous prendre raw
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 avantage pour les 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 tels que le traitement des options de protocole, il peut devenir assez vieux d'avoir à définir individuellement des options ou à utiliser plusieurs paramètres pour transférer des états intermédiaires pour effectuer un résultat final.
Mais parfois définir drapeau.blah et avoir la liste popup dans votre IDE est génial surtout si vous aimez moi et ne vous souvenez pas du nom du drapeau que vous voulez définir sans constamment référencer la liste.
Personnellement, je vais parfois hésiter à déclarer des types booléens car à un moment donné, je vais me retrouver avec l'impression erronée que le champ que je viens de basculer n'était pas dépendant (pensez à la concurrence multi-thread) du statut r/w d'autres champs "apparemment" non liés qui partagent le même mot 32 bits.
Mon vote est que cela dépend de la contexte de la situation et dans certains cas, les deux approches peuvent fonctionner très bien.
Il est sujet aux erreurs, Oui. J'ai vu beaucoup d'erreurs dans ce type de code, principalement parce que certaines personnes pensent qu'elles devraient jouer avec elle et la logique métier d'une manière totalement désorganisée, créant des cauchemars de maintenance. Ils pensent que les" vrais " programmeurs peuvent écrire value |= mask;
, value &= ~mask;
ou encore pire des choses à n'importe quel endroit, et c'est juste ok. Encore mieux s'il y a un opérateur d'incrément autour, un couple de memcpy
, des lancers de pointeur et toute syntaxe obscure et sujette aux erreurs arrive à venir leur esprit à l'époque. Bien sûr, il n'y a pas besoin d'être cohérent et vous pouvez retourner des bits de deux ou trois manières différentes, distribuées aléatoirement.
Mon conseil serait:
- Encapsuler ce ---- dans une classe, avec des méthodes telles que
SetBit(...)
etClearBit(...)
. (Si vous n'avez pas de classes en C, dans un module.) Pendant que vous y êtes, vous pouvez documenter tout leur comportement. - test unitaire de cette classe ou module.
Votre première méthode est préférable, à mon humble avis. Pourquoi obscurcir le problème? Le Tripotage est une chose très basique. C le fait correctement. L'endianesse n'a pas d'importance. La seule chose que fait la solution syndicale est de 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 "opérateurs c"
Les trois premières pages sont:
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
.. donc je pense que cet argument sur les gens nouveaux dans la langue est un peu stupide.
J'utilise presque toujours les opérations logiques avec un masque de bits, soit directement, soit en tant que macro. par exemple
#define ASSERT_GPS_RESET() { P1OUT &= ~GPS_RESET ; }
Incidemment, votre définition d'union dans la question originale ne fonctionnerait pas sur ma combinaison processeur / compilateur. Le type int n'a que 16 bits de large et les définitions de bitfield sont 32. Pour le rendre légèrement plus portable vous devez définir un nouveau type de 32 bits que vous pouvez ensuite mapper au type de base requis sur chaque architecture cible dans le cadre du portage exercice. 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;
L'int superposé 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 rester simple.
Une fois que vous êtes habitué, l'utilisation de masques est simple, sans ambiguïté et portable.
Bitfields sont simples, mais ils ne sont pas portables sans avoir à faire un travail supplémentaire.
Si jamais vous devez écrire du code conforme à MISRA , les directives MISRA froncent les sourcils sur les champs de bits, les unions et bien d'autres aspects de C, afin d'éviter les indéfini ou comportement dépendant de la mise en œuvre.
Généralement, celui qui est plus facile à lire et à comprendre est celui qui est également 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 géniaux, 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 déclaration. Mais le code ordinaire généré par un compilateur n'essaiera pas de faire |= atomic.
Donc, si plusieurs threads exécutent différentes opérations de bits set, l'une des opérations de Bits set peut être fausse. Puisque les deux threads vont exécuter:
thread 1 thread 2
LOAD field LOAD field
OR mask1 OR mask2
STORE field STORE field
Le résultat peut être field' = champ OU mask1 OU mask2 (adresse), ou le résultat peut être field' = champ OU mask1 (pas d'adresse) ou le résultat peut être field' = champ OU mask2 (pas prévu).