Manipolazione bitfield in C

Il classico problema di testare e impostare singoli bit in un intero in C è forse una delle più comuni abilità di programmazione di livello intermedio. Puoi impostare e testare con semplici maschere di bit come

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

Un post interessante sul blog sostiene che questo è sobject a errori, difficile da mantenere e scarsa pratica. Il linguaggio C stesso fornisce un accesso a livello di bit che è tipicamente sicuro e portatile:

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

Ma questo mi fa rabbrividire .

La discussione interessante che il mio collega e io abbiamo avuto su questo è ancora irrisolta. Entrambi gli stili funzionano e ritengo che il classico metodo bitmask sia semplice, sicuro e chiaro. Il mio collega è d’accordo che sia comune e facile, ma il metodo dell’unione bitfield vale le poche righe in più per renderlo portatile e più sicuro.

Ci sono altri argomenti per entrambe le parti? In particolare c’è qualche ansible fallimento, forse con endianness, che il metodo della maschera di bit possa mancare ma dove il metodo della struttura è sicuro?

I bitfield non sono così portabili come pensi, dato che “C non fornisce alcuna garanzia sull’ordinamento dei campi all’interno delle parole della macchina” ( The C book )

Ignorandolo, usato correttamente , entrambi i metodi sono sicuri. Entrambi i metodi consentono anche l’accesso simbolico a variabili integrali. Puoi sostenere che il metodo bitfield è più facile da scrivere, ma significa anche più codice da esaminare.

Se il problema è che l’impostazione e la cancellazione dei bit è soggetta a errori, la cosa giusta da fare è scrivere funzioni o macro per assicurarsi di farlo correttamente.

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

Il che rende leggibile il tuo codice se non ti dispiace che val debba essere un lvalue ad eccezione di BIT_IS_SET. Se ciò non ti rende felice, allora prendi l'incarico, lo metti in parentesi e lo usi come val = SET_BIT (val, someIndex); che sarà equivalente.

In realtà, la risposta è di considerare il disaccoppiamento di ciò che vuoi da come lo vuoi fare.

I bitfield sono fantastici e di facile lettura, ma sfortunatamente il linguaggio C non specifica il layout dei bitfield in memoria , il che significa che sono sostanzialmente inutili per gestire i dati compressi nei formati su disco o nei protocolli binari. Se me lo chiedi, questa decisione è stata un errore di progettazione in C-Ritchie avrebbe potuto scegliere un ordine e bloccato con esso.

Devi pensare a questo dal punto di vista di uno scrittore: conosci il tuo pubblico. Quindi ci sono un paio di “pubblici” da considerare.

Per prima cosa c’è il programmatore C classico, che ha mascherato le loro intere vite e potrebbe farlo nel sonno.

In secondo luogo c’è il newb, che non ha idea di cosa sia questo |, e cosa sia. Stavano programmando php al loro ultimo lavoro e ora lavorano per te. (Lo dico come un newb che fa php)

Se scrivi per soddisfare il primo pubblico (che è tutto il giorno-bit-maschera), li renderai molto felici, e saranno in grado di mantenere il codice bendato. Tuttavia, il newb dovrà probabilmente superare una grande curva di apprendimento prima che siano in grado di mantenere il proprio codice. Avranno bisogno di conoscere gli operatori binari, come si usano queste operazioni per impostare / cancellare i bit, ecc. Quasi certamente si avranno dei bug introdotti dal newb come tutti i trucchi necessari per farlo funzionare.

D’altra parte, se scrivi per soddisfare il secondo pubblico, i newb avranno un tempo più semplice per mantenere il codice. Avranno un tempo più facile di groking

  flags.force = 0; 

di

  flags &= 0xFFFFFFFE; 

e il primo pubblico diventerà scontroso, ma è difficile immaginare che non sarebbero in grado di ingannare e mantenere la nuova syntax. È molto più difficile sbagliare. Non ci saranno nuovi bug, perché il newb manterrà più facilmente il codice. Riceverai solo conferenze su come “ai miei tempi avevi bisogno di una mano ferma e di un ago magnetizzato per fare pezzi … non avevamo nemmeno maschere di bit!” (grazie XKCD ).

Quindi consiglio vivamente di usare i campi sopra le maschere di bit per rendere sicuro il tuo codice.

L’utilizzo del sindacato ha un comportamento indefinito secondo lo standard ANSI C e, quindi, non dovrebbe essere usato (o almeno non essere considerato portatile).

Dallo standard ISO / IEC 9899: 1999 (C99) :

Allegato J – Problemi di portabilità:

1 I seguenti non sono specificati:

– Il valore dei byte di riempimento quando si memorizzano valori in strutture o unioni (6.2.6.1).

– Il valore di un membro del sindacato diverso da quello memorizzato in (6.2.6.1).

6.2.6.1 – Concetti linguistici – Rappresentazione dei tipi – Generale:

6 Quando un valore è memorizzato in un object di tipo struttura o unione, incluso in un object membro, i byte della rappresentazione dell’object che corrispondono a qualsiasi byte di riempimento prendono valori non specificati. [42]) Il valore di una struttura o di un object unione è mai una rappresentazione trap, anche se il valore di un membro della struttura o dell’object union può essere una rappresentazione trap.

7 Quando un valore è memorizzato in un membro di un object di tipo union, i byte della rappresentazione dell’object che non corrispondono a quel membro ma corrispondono ad altri membri prendono valori non specificati.

Quindi, se si desidera mantenere la corrispondenza di bitfield ↔ numero intero e mantenere la portabilità, suggerisco caldamente di utilizzare il metodo bitmasking, che, contrariamente al post del blog collegato, non è una ctriggers pratica.

Che cos’è l’approccio bitfield che ti fa rabbrividire?

Entrambe le tecniche hanno il loro posto e l’unica decisione che ho è quale usare:

Per semplici bit “one-off”, io uso direttamente gli operatori bit a bit.

Per qualcosa di più complesso, ad esempio le mappe dei registri hardware, l’approccio bitfield vince a mani basse.

  • I bitfield sono più succinti da usare (a scapito di / leggermente / più verbosità da scrivere.
  • I bitfield sono più robusti (comunque la dimensione è “int”)
  • I bitfield di solito sono altrettanto veloci degli operatori bit a bit.
  • I bitfield sono molto potenti quando si dispone di un mix di campi a bit singoli e multipli e l’estrazione del campo a più bit comporta un sacco di spostamenti manuali.
  • I bitfield sono effettivamente auto-documentanti. Definendo la struttura e quindi nominando gli elementi, so cosa si intende fare.
  • I bitfield gestiscono anche strutture più grandi di un singolo int.
  • Con gli operatori bit a bit, la pratica tipica (ctriggers) è una sfilza di #definiti per le maschere di bit.

  • L’unica avvertenza con bitfield è assicurarsi che il compilatore abbia davvero imballato l’object nella dimensione desiderata. Non ricordo se questo è definito dallo standard, quindi un assert (sizeof (myStruct) == N) è un controllo utile.

In ogni caso, i bitfield sono stati usati nel software GNU per decenni e non ha fatto loro alcun danno. Mi piacciono come parametri per le funzioni.

Direi che i bitfield sono convenzionali rispetto alle strutture. Tutti sanno come AND i valori per impostare varie opzioni e il compilatore riduce questo a operazioni bitwise molto efficienti sulla CPU.

Fornendo l’uso delle maschere e dei test nel modo corretto, le astrazioni fornite dal compilatore dovrebbero renderlo robusto, semplice, leggibile e pulito.

Quando ho bisogno di un set di interruttori on / off, ho intenzione di continuare a usarli in C.

Il post sul blog si riferisce a menzioni campo unione raw come metodo di accesso alternativo per bitfield.

Gli scopi dell’autore del post sul blog usati come raw sono ok, tuttavia se si prevede di utilizzarlo per qualsiasi altra cosa (ad es. Serializzazione di campi di bit, impostazione / controllo di singoli bit), il disastro ti sta solo aspettando dietro l’angolo. L’ordinamento dei bit in memoria dipende dall’architettura e le regole del paddle di memoria variano dal compilatore al compilatore (vedi wikipedia ), quindi la posizione esatta di ogni bitfield può differire, in altre parole non si può essere sicuri di quale bit di raw ogni bitfield corrisponda.

Tuttavia, se non hai intenzione di mescolarlo, è meglio che tu diventi raw e sarai al sicuro.

Bene, non si può sbagliare con la mapping della struttura poiché entrambi i campi sono accessibili e possono essere usati in modo intercambiabile.

Un vantaggio per i campi bit è che puoi facilmente aggregare le opzioni:

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

In alcuni ambienti, ad esempio le opzioni del protocollo, può diventare piuttosto scadente dover impostare singolarmente le opzioni o utilizzare più parametri per traghettare gli stati intermedi per ottenere un risultato finale.

Ma a volte impostando flag.blah e avendo la lista popup nel tuo IDE è grandioso specialmente se ti piaccio e non ricordi il nome della bandiera che vuoi impostare senza fare costantemente riferimento alla lista.

Personalmente a volte mi sottrarrò dal dichiarare i tipi booleani perché a un certo punto finirò con l’impressione errata che il campo che ho appena triggersto non dipendesse (pensate alla concorrenza multi-thread) sullo stato di r / w dell’altro “apparentemente” campi non correlati che capita di condividere la stessa parola a 32 bit.

Il mio voto è che dipende dal contesto della situazione e in alcuni casi entrambi gli approcci possono funzionare alla grande.

In C ++, basta usare std::bitset .

È sobject a errori, sì. Ho visto molti errori in questo tipo di codice, principalmente perché alcune persone sentono che dovrebbero pasticciare con essa e la logica di business in modo totalmente disorganizzato, creando incubi di manutenzione. Pensano che i programmatori “reali” possano scrivere value |= mask; , value &= ~mask; o anche cose peggiori in qualsiasi posto, e questo è ok. Ancora meglio se ci sono alcuni operatori di incremento in giro, un paio di memcpy puntatori di memcpy e qualsiasi syntax oscura e soggetta a errori capita di venire in mente in quel momento. Ovviamente non è necessario essere coerenti e si possono capovolgere i bit in due o tre modi diversi, distribuiti casualmente.

Il mio consiglio sarebbe:

  1. Incapsula questo —- in una class, con metodi come SetBit(...) e ClearBit(...) . (Se non hai classi in C, in un modulo.) Mentre ci sei, puoi documentare tutto il loro comportamento.
  2. Unit test di class o modulo.

Il tuo primo metodo è preferibile, IMHO. Perché offuscare il problema? Un po ‘di giocherellare è una cosa molto semplice. C ha fatto bene. L’endianità non ha importanza. L’unica cosa che fa la soluzione sindacale è nominare le cose. 11 potrebbe essere misterioso, ma dovrebbe essere sufficiente definire un nome significativo o enumerato.

I programmatori che non sono in grado di gestire i fondamentali come “| & ^ ~” hanno probabilmente una linea di lavoro sbagliata.

Quando google per “c operatori”

Le prime tre pagine sono:

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 penso che l’argomento sulle persone nuove nella lingua sia un po ‘sciocco.

Quasi sempre utilizzo le operazioni logiche con un bit mask, direttamente o come macro. per esempio

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

per inciso, la tua definizione di unione nella domanda originale non funzionerebbe sulla mia combinazione processore / compilatore. Il tipo int ha solo 16 bit di larghezza e le definizioni di bitfield sono 32. Per renderlo leggermente più portatile, è necessario definire un nuovo tipo a 32 bit che è ansible quindi associare al tipo di base richiesto su ogni architettura di destinazione come parte del Esercizio di porting. Nel mio caso

 typedef unsigned long int uint32_t 

e nell’esempio originale

 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 overlaid dovrebbe anche essere reso non firmato.

Beh, suppongo che sia un modo per farlo, ma preferirei sempre mantenerlo semplice .

Una volta che ci sei abituato, usare le maschere è semplice, non ambiguo e portatile.

I bitfield sono semplici, ma non sono portabili senza dover fare altro lavoro.

Se mai dovessi scrivere codice conforms a MISRA , le linee guida MISRA disapprovano i bitfield, i sindacati e molti altri aspetti di C, al fine di evitare comportamenti non definiti o dipendenti dall’implementazione.

Generalmente, quello che è più facile da leggere e capire è quello che è anche più facile da mantenere. Se hai colleghi che sono nuovi in ​​C, l’approccio “più sicuro” sarà probabilmente quello più facile da capire per loro.

I bitfield sono grandiosi, tranne per il fatto che le operazioni di manipolazione dei bit non sono atomiche e possono pertanto causare problemi nell’applicazione multi-thread.

Ad esempio si potrebbe assumere che una macro:

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

Definisce un'operazione atomica, poiché | = è un'istruzione. Ma il codice ordinario generato da un compilatore non cercherà di rendere | = atomico.

Quindi, se più thread eseguono operazioni di bit di set differenti, una delle operazioni bit di bit potrebbe essere spuria. Poiché entrambi i thread verranno eseguiti:

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

Il risultato può essere campo '= campo O maschera1 OR maschera2 (intento), oppure il risultato può essere campo' = campo OR maschera1 (non intenzionato) o il risultato può essere campo '= campo O maschera2 (non previsto).