Le problème classique de test et de 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 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 fait valoir que cela est sujet aux erreurs, difficile à maintenir et à une mauvaise pratique. Le langage C lui-même fournit un accès au niveau des bits qui est sûr 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'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 masque de bits est simple, sûre et claire. Mon collègue convient que c'est commun et facile, mais la méthode d'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'un ou l'autre côté? En particulier, y a-t-il un échec possible, peut-être avec endianité, que la méthode du masque de bits peut manquer, mais où la méthode de structure est sûre?
Les champs binaires ne sont pas aussi portables que vous le pensez, car "C ne donne aucune garantie de l'ordre des champs dans les mots machine" ( Le livre C )
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 affirmer que la méthode du champ de bits est plus facile à écrire, mais cela signifie également plus de code à réviser.
Si le problème est que la définition et la suppression des bits sont sujettes aux erreurs, la bonne chose à faire est d'écrire des fonctions ou des macros pour vous assurer que vous le faites 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 valeur l, à l'exception de BIT_IS_SET. Si cela ne vous satisfait pas, vous retirez 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 dissocier ce que vous voulez de la façon dont vous voulez le faire.
Les champs binaires sont grands et faciles à lire, mais malheureusement le langage C ne spécifie pas la disposition des champs binaires 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 de fils binaires . Si vous me demandez, cette décision était une erreur de conception dans C — Ritchie aurait pu choisir une commande et la respecter.
Vous devez y penser du point de vue d'un écrivain - connaître votre public. Il y a donc deux "publics" à considérer.
Il y a d'abord le programmeur C classique, qui a masqué toute sa vie et pourrait le faire dans son sommeil.
Deuxièmement, il y a le newb, qui n'a aucune idée de tout cela |, et des trucs. Ils programmaient php à leur dernier emploi et maintenant ils travaillent pour vous. (Je dis cela comme un newb qui fait du php)
Si vous écrivez pour satisfaire le premier public (c'est-à-dire tout au long de la journée), vous les rendrez très heureux et ils pourront maintenir le code les yeux bandés. Cependant, le newb devra probablement surmonter une grande courbe d'apprentissage avant de pouvoir maintenir votre code. Ils auront besoin de se renseigner sur les opérateurs binaires, comment vous utilisez ces opérations pour définir/effacer les bits, etc.
D'un autre côté, si vous écrivez pour satisfaire le deuxième public, les newbs auront plus de facilité à maintenir le code. Ils auront plus de facilité à gronder
flags.force = 0;
que
flags &= 0xFFFFFFFE;
et le premier public deviendra juste grincheux, mais il est difficile d'imaginer qu'ils ne pourraient pas grogner et maintenir la nouvelle syntaxe. C'est juste beaucoup plus difficile à foutre. Il n'y aura pas de nouveaux bugs, car le newb maintiendra plus facilement le code. Vous allez juste recevoir des conférences sur la façon dont "à l'époque, j'avais besoin d'une main ferme et d'une aiguille aimantée pour régler les bits ... nous n'avions même pas de masques de bit!" (merci XKCD ).
Je recommande donc fortement d'utiliser les champs au-dessus des masques de bit pour sécuriser newb 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 - 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 enregistré dans (6.2.6.1).
6.2.6.1 - Concepts du langage - 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 objet membre, les octets de la représentation d'objet correspondant à tout octet de remplissage prennent des valeurs non spécifiées. [42]) La valeur d'une structure ou d'un objet union est jamais une représentation d'interruption, même si la valeur d'un membre de la structure ou de l'objet union peut être une représentation d'interruption.
7 Lorsqu'une valeur est stockée dans un membre d'un objet de type union, les octets de la représentation d'objet qui ne correspondent pas à ce membre mais correspondent à d'autres membres prennent des valeurs non spécifiées.
Donc, si vous voulez conserver la correspondance entière du champ de bits and et conserver la portabilité, je vous suggère fortement d'utiliser la méthode de masquage de bit, contrairement à l'article de blog lié, ce n'est pas mauvaise pratique.
Qu'est-ce que l'approche bitfield 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 violon "unique", j'utilise directement les opérateurs au niveau du bit.
Pour tout ce qui est plus complexe - par exemple, les cartes de registres matériels, l'approche bitfield gagne haut la main.
Avec les opérateurs au niveau du bit, une (mauvaise) pratique 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 vraiment compressé l'objet à la taille souhaitée. Je ne me souviens pas si cela est défini par la norme, donc un assert (sizeof (myStruct) == N) est une vérification utile.
Quoi qu'il en soit, les champs binaires ont été utilisés dans le logiciel GNU pendant des décennies et cela ne leur a fait aucun mal. Je les aime comme paramètres des fonctions.
Je dirais que les champs de bits sont conventionnels par opposition aux structures. Tout le monde sait comment ET les valeurs pour désactiver diverses options et le compilateur se résume à très efficace opérations au niveau du bit sur le CPU.
Si vous utilisez correctement les masques et les tests, les abstractions fournies par le compilateur devraient 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.
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 être assez ancien de devoir définir des options individuellement ou d'utiliser plusieurs paramètres pour transporter des é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 génial, surtout si vous m'aimez et que vous ne vous souvenez pas du nom du drapeau que vous souhaitez définir sans référencer constamment la liste.
Personnellement, je vais parfois hésiter à déclarer des types booléens, car à un moment donné, je finirai avec l'impression erronée que le champ que je viens de basculer ne dépendait pas (pensez à la concurrence multi-thread) du statut r/w des autres "en apparence" champs non liés qui se trouvent partager le même mot 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.
Le article de blog vous faites référence au champ d'union raw
comme méthode d'accès alternative pour les champs de bits.
Les objectifs que l'auteur du billet de blog a utilisés pour raw
sont corrects, mais si vous prévoyez de l'utiliser pour autre chose (par exemple, sérialisation de champs de bits, définition/vérification de bits individuels), un désastre vous attend juste au coin de la rue. L'ordre des bits en mémoire dépend de l'architecture et les règles de remplissage de mémoire varient d'un compilateur à l'autre (voir wikipedia ), la position exacte de chaque champ binaire peut donc différer, en d'autres termes, vous ne pouvez jamais être sûr de quel bit de raw
à chaque champ de bits correspond.
Cependant, si vous ne prévoyez pas de le mélanger, vous feriez mieux de supprimer raw
et vous serez en sécurité.
En C++, utilisez simplement std::bitset<N>
.
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 lui 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 value |= mask;
, value &= ~mask;
Ou pire encore à n'importe quel endroit, et c'est tout simplement correct. Encore mieux s'il y a un opérateur d'incrémentation autour, quelques memcpy
, des lanceurs de pointeurs et toute syntaxe obscure et sujette aux erreurs qui lui viennent à 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, réparties de manière aléatoire.
Mon conseil serait:
SetBit(...)
et ClearBit(...)
. (Si vous n'avez pas de classes en C, dans un module.) Pendant que vous y êtes, vous pouvez documenter tout leur comportement.Votre première méthode est préférable, à mon humble avis. Pourquoi masquer le problème? Le violonage des bits est une chose vraiment basique. C l'a bien fait. L'endianisme n'a pas d'importance. La seule chose que fait la solution syndicale est de nommer les choses. 11 pourrait être mystérieux, mais #defined à un nom significatif ou énuméré devrait suffire.
Les programmeurs qui ne peuvent pas gérer les fondamentaux comme "| & ^ ~" sont probablement dans la mauvaise direction.
Quand je google pour les "opérateurs c"
Les trois premières pages sont:
http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2Bhttp://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/V40F_HTML/AQTLTBTE/ DOCU_059.HTMhttp://www.cs.mun.ca/~michael/c/op.html
.. donc je pense que l'argument sur les gens nouveaux dans la langue est un peu idiot.
Eh bien, je suppose que c'est une façon de le faire, mais je préférerais toujours garder les choses simples .
Une fois que vous y êtes habitué, l'utilisation de 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 MISRA - du code conforme, les directives MISRA froncent les sourcils sur les champs de bits, les unions et de nombreux autres aspects de C, afin d'éviter comportement indéfini ou dépendant de l'implémentation.
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 ; }
d'ailleurs votre définition d'union dans la question d'origine ne fonctionnerait pas sur ma combinaison processeur/compilateur. Le type int ne fait que 16 bits de large et les définitions de champs binaires sont 32. Pour le rendre légèrement plus portable, vous devez alors définir un nouveau type 32 bits que vous pouvez ensuite mapper au type de base requis sur chaque architecture cible dans le cadre de la exercice de portage. Dans mon cas
typedef unsigned long int uint32_t
et dans l'exemple d'origine
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 entier superposé doit également être rendu non signé.
Généralement, celui qui est plus facile à lire et à comprendre est celui qui est aussi plus facile à entretenir. 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 binaires 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 multithreads.
Par exemple, on pourrait supposer qu'une macro:
#define SET_BIT(val, bitIndex) val |= (1 << bitIndex)
Définit une opération atomique, car | = est une instruction. Mais le code ordinaire généré par un compilateur n'essaiera pas de rendre | = atomique.
Donc, si plusieurs threads exécutent différentes opérations de définition de bits, l'une des opérations de définition de bits peut être fausse. Puisque les deux threads s'exécuteront:
thread 1 thread 2
LOAD field LOAD field
OR mask1 OR mask2
STORE field STORE field
Le résultat peut être champ '= champ OR masque1 OR masque2 (intentionnel), ou le résultat peut être champ' = champ OR mask1 (non intentionnel) ou le résultat peut être field '= field OR mask2 (non prévu).