Perché l'assegnazione di un valore a un campo di bit non restituisce lo stesso valore?


96

Ho visto il codice seguente in questo post di Quora :

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = 1;
  if(s.enabled == 1)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

Sia in C che in C ++, l'output del codice è inaspettato ,

È disabilitato !!

Sebbene la spiegazione relativa al "bit di segno" sia fornita in quel post, non sono in grado di capire come sia possibile che impostiamo qualcosa e poi non rifletta così com'è.

Qualcuno può dare una spiegazione più elaborata?


Nota : entrambi i tag & sono obbligatori, perché i loro standard differiscono leggermente per la descrizione dei campi di bit. Vedi le risposte per la specifica C e la specifica C ++ .


46
Poiché il bitfield è dichiarato come intpenso che possa contenere solo i valori 0e -1.
Osiride

6
basta pensare a come int memorizza -1. Tutti i bit sono impostati su 1. Quindi, se hai solo un bit, deve chiaramente essere -1. Quindi 1 e -1 nel 1 bit int sono uguali. Cambia il segno di spunta in "if (s.enabled! = 0)" e funziona. Perché 0 non può essere.
Jürgen

3
È vero che queste regole sono le stesse in C e C ++. Tuttavia, in base alle norme sull'utilizzo dei tag , dovremmo taggarlo solo come C e astenerci dal cross-tagging quando non necessario. Rimuoverò la parte C ++, non dovrebbe influire sulle risposte pubblicate.
Lundin

8
Hai provato a cambiarlo in struct mystruct { unsigned int enabled:1; };?
ChatterOne

4
Si prega di leggere le politiche sui tag C e C ++ , in particolare la parte relativa al cross-tagging C e C ++, stabilite qui attraverso il consenso della comunità . Non entrerò in una guerra di rollback, ma questa domanda è contrassegnata in modo errato C ++. Anche se i linguaggi hanno qualche leggera differenza a causa di vari TC, allora fai una domanda separata sulla differenza tra C e C ++.
Lundin

Risposte:


78

I campi di bit sono incredibilmente poco definiti dallo standard. Dato questo codice struct mystruct {int enabled:1;};, allora non sappiamo:

  • Quanto spazio occupa questo - se ci sono bit / byte di riempimento e dove si trovano in memoria.
  • Dove si trova il bit in memoria. Non definito e dipende anche dall'endianess.
  • Indica se un int:ncampo di bit deve essere considerato come firmato o non firmato.

Per quanto riguarda l'ultima parte, C17 6.7.2.1/10 dice:

Un campo di bit viene interpretato come avente un tipo intero con segno o senza segno costituito dal numero di bit specificato 125)

Nota non normativa che spiega quanto sopra:

125) Come specificato in 6.7.2 sopra, se l'effettivo identificatore di tipo utilizzato è into un nome typedef definito come int, allora è definito dall'implementazione se il campo bit è con segno o senza segno.

Nel caso in cui il campo di signed intbit debba essere considerato come e si crea un po 'di dimensione 1, non c'è spazio per i dati, solo per il bit di segno. Questo è il motivo per cui il tuo programma potrebbe dare risultati strani su alcuni compilatori.

Buona pratica:

  • Non utilizzare mai campi di bit per nessuno scopo.
  • Evita di utilizzare il inttipo con segno per qualsiasi forma di manipolazione dei bit.

5
Al lavoro abbiamo static_asserts sulla dimensione e l'indirizzo dei bitfield solo per assicurarci che non siano riempiti. Usiamo i bitfield per i registri hardware nel nostro firmware.
Michael

4
@Lundin: La cosa brutta con le maschere e gli offset # define-d è che il tuo codice viene disseminato di spostamenti e operatori AND / OR bit-wise. Con i bitfield il compilatore si occupa di questo per te.
Michael

4
@Michael Con i bitfield il compilatore si occupa di questo per te. Bene, va bene se i tuoi standard per "si prende cura di quello" sono "non portabile" e "imprevedibile". I miei sono più alti di così.
Andrew Henle

3
@AndrewHenle Leushenko sta dicendo che dal punto di vista dello standard C stesso , spetta all'implementazione scegliere o meno di seguire l'ABI x86-64 o meno.
mtraceur

3
@ AndrewHenle Giusto, sono d'accordo su entrambi i punti. Il punto era che penso che il tuo disaccordo con Leushenko si riduca al fatto che stai usando "implementazione definita" per riferirsi solo a cose né strettamente definite dallo standard C né strettamente definite dalla piattaforma ABI, e lui la usa per fare riferimento a tutto ciò che non è strettamente definito dal solo standard C.
mtraceur

58

Non riesco a capire come sia possibile che impostiamo qualcosa e poi non si presenti così com'è.

Stai chiedendo perché si compila e ti dà un errore?

Sì, idealmente dovrebbe darti un errore. E lo fa, se usi gli avvisi del tuo compilatore. In GCC, con -Werror -Wall -pedantic:

main.cpp: In function 'int main()':
main.cpp:7:15: error: overflow in conversion from 'int' to 'signed char:1' 
changes value from '1' to '-1' [-Werror=overflow]
   s.enabled = 1;
           ^

Il motivo per cui questo viene lasciato alla definizione dell'implementazione rispetto a un errore potrebbe avere più a che fare con gli usi storici, dove richiedere un cast significherebbe rompere il vecchio codice. Gli autori dello standard potrebbero ritenere che gli avvertimenti fossero sufficienti per aumentare la tensione per gli interessati.

Per aggiungere un po 'di prescrittivismo, farò eco all'affermazione di @ Lundin: "Non usare mai campi di bit per nessuno scopo". Se hai il tipo di buone ragioni per ottenere un livello basso e specifico sui dettagli del layout della memoria che ti farebbero pensare che avevi bisogno di bitfield in primo luogo, gli altri requisiti associati che quasi certamente hai si scontreranno con la loro sottospecificazione.

(TL; DR - Se sei abbastanza sofisticato da "necessitare" legittimamente di campi di bit, non sono sufficientemente definiti per servirti.)


15
Gli autori dello standard erano in vacanza il giorno in cui è stato progettato il capitolo dei campi di bit. Quindi il bidello doveva farlo. Non vi è alcun fondamento logico su come sono progettati i campi di bit.
Lundin

9
Non esiste una logica tecnica coerente . Ma questo mi porta a concludere che c'era una logica politica : evitare di rendere errato il codice o le implementazioni esistenti. Ma il risultato è che c'è molto poco sui bitfield su cui puoi fare affidamento.
John Bollinger

6
@JohnBollinger C'era decisamente una politica in atto, che ha causato molti danni a C90. Una volta ho parlato con un membro del comitato che ha spiegato l'origine di molte stronzate: lo standard ISO non poteva essere autorizzato a favorire alcune tecnologie esistenti. Questo è il motivo per cui siamo bloccati con cose stupide come il supporto per il complemento a 1 e la grandezza del segno char, la firma definita dall'implementazione , il supporto per byte che non sono 8 bit, ecc. Non era permesso dare ai computer idioti uno svantaggio di mercato.
Lundin

1
@ Lundin Sarebbe interessante vedere una raccolta di commenti e autopsie di persone che credevano che i compromessi fossero stati fatti per errore e perché. Mi chiedo quanto studio di questi "l'abbiamo fatto l'ultima volta, e ha funzionato / non ha funzionato" sia diventato conoscenza istituzionale per informare il prossimo caso del genere, contro solo storie nella testa delle persone.
HostileFork dice di non fidarsi di SE

1
Questo è ancora elencato come punto n. 1 dei principi originali di C nella Carta C2x: "Il codice esistente è importante, le implementazioni esistenti non lo sono." ... "nessuna implementazione è stata considerata come l'esempio con cui definire C: Si presume che tutte le implementazioni esistenti debbano cambiare in qualche modo per conformarsi allo Standard."
Leushenko

23

Questo è il comportamento definito dall'implementazione. Sto assumendo che le macchine su cui stai eseguendo questo utilizzo utilizzino interi con segno di complimento a due e trattino intin questo caso come un intero con segno per spiegare perché non inserisci if true parte dell'istruzione if.

struct mystruct { int enabled:1; };

dichiara enablecome un campo di bit a 1 bit. Poiché è firmato, i valori validi sono -1e 0. L'impostazione del campo su 1trabocca quel bit che torna a -1(questo è un comportamento indefinito)

Essenzialmente quando si ha a che fare con un campo di bit con segno il valore massimo è 2^(bits - 1) - 1che è 0in questo caso.


"una volta firmato, i valori validi sono -1 e 0". Chi ha detto che è firmato? Non è un comportamento definito ma definito dall'implementazione. Se è firmato, i valori validi sono -e +. Il complemento di 2 non ha importanza.
Lundin

5
@Lundin Un numero di complimento a 1 bit di due ha solo due possibili valori. Se il bit è impostato, poiché è il bit di segno, è -1. Se non è impostato, è "positivo" 0. So che questa è l'implementazione definita, sto solo spiegando i risultati utilizzando l'impianto più comune
NathanOliver

1
La chiave qui è piuttosto che il complemento di 2 o qualsiasi altra forma firmata non può funzionare con un singolo bit disponibile.
Lundin

1
@ JohnBollinger Lo capisco. Ecco perché ho il discliamer che questa è l'implementazione definita. Almeno per i 3 grandi, intin questo caso trattano tutti come firmati. È un peccato che i campi di bit siano così sotto specificati. È fondamentalmente questa funzionalità, consulta il tuo compilatore su come usarla.
NathanOliver

1
@Lundin, la formulazione dello standard per la rappresentazione di interi con segno può gestire perfettamente il caso in cui ci sono bit di valore zero, almeno in due delle tre alternative consentite. Funziona perché assegna valori di posizione (negativi) ai bit di segno, piuttosto che fornire loro un'interpretazione algoritmica.
John Bollinger

10

Potresti pensare che nel sistema del complemento a 2, il bit più a sinistra è il bit del segno. Qualsiasi numero intero con segno con il bit più a sinistra impostato è quindi un valore negativo.

Se hai un intero con segno a 1 bit, ha solo il bit di segno. Quindi l'assegnazione 1a quel singolo bit può impostare solo il bit di segno. Quindi, quando lo rileggi, il valore viene interpretato come negativo e così è -1.

I valori che un intero con segno a 1 bit può contenere è -2^(n-1)= -2^(1-1)= -2^0= -1e2^n-1= 2^1-1=0


8

Come per lo standard C ++ n4713 , viene fornito uno snippet di codice molto simile. Il tipo utilizzato è BOOL(personalizzato), ma può essere applicato a qualsiasi tipo.

12.2.4

4 Se il valore vero o falso è memorizzato in un campo di bitbooldi qualsiasi dimensione (incluso un campo di bit di un bit), ilboolvaloreoriginalee il valore del campo di bit devono essere confrontati uguali. Se il valore di un enumeratore è memorizzato in un campo di bit dello stesso tipo di enumerazione e il numero di bit nel campo di bit è abbastanza grande da contenere tutti i valori di quel tipo di enumerazione (10.2), il valore dell'enumeratore originale e il il valore del campo di bit deve confrontare uguale . [ Esempio:

enum BOOL { FALSE=0, TRUE=1 };
struct A {
  BOOL b:1;
};
A a;
void f() {
  a.b = TRUE;
  if (a.b == TRUE)    // yields true
    { /* ... */ }
}

- esempio finale]


A prima vista, la parte in grassetto sembra aperta all'interpretazione. Tuttavia, l'intento corretto diventa chiaro quando enum BOOLviene derivato da int.

enum BOOL : int { FALSE=0, TRUE=1 }; // ***this line
struct mystruct { BOOL enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = TRUE;
  if(s.enabled == TRUE)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

Con il codice sopra dà un avviso senza -Wall -pedantic:

avviso: "mystruct :: enabled" è troppo piccolo per contenere tutti i valori di "enum BOOL" struct mystruct { BOOL enabled:1; };

L'output è:

È disabilitato !! (quando si usa enum BOOL : int)

Se enum BOOL : intè reso semplice enum BOOL, l'output è come il passaggio standard sopra specificato:

È abilitato (durante l'utilizzo enum BOOL)


Quindi, si può concludere, anche come poche altre risposte hanno fatto, che il inttipo non è abbastanza grande da memorizzare il valore "1" in un solo campo di bit.


0

Non c'è niente di sbagliato nella tua comprensione dei campi di bit che posso vedere. Quello che vedo è che hai ridefinito prima mystruct come struct mystruct {int enabled: 1; } e poi come struct mystruct s; . Quello che avresti dovuto codificare era:

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
    mystruct s; <-- Get rid of "struct" type declaration
    s.enabled = 1;
    if(s.enabled == 1)
        printf("Is enabled\n"); // --> we think this to be printed
    else
        printf("Is disabled !!\n");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.