Dlaczego przypisanie wartości do pola bitowego nie zwraca tej samej wartości z powrotem?


96

Widziałem poniższy kod w tym poście 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");
}

Zarówno w C, jak i C ++ wynik kodu jest nieoczekiwany ,

Jest niepełnosprawny !!

Chociaż wyjaśnienie związane z "bitem znaku" jest podane w tym poście, nie jestem w stanie zrozumieć, jak to możliwe, że coś ustawiliśmy, a potem nie odzwierciedla tego, czym jest.

Czy ktoś może podać bardziej rozbudowane wyjaśnienie?


Uwaga : oba tagi & są wymagane, ponieważ ich standardy nieco się różnią przy opisywaniu pól bitowych. Zobacz odpowiedzi dotyczące specyfikacji C i specyfikacji C ++ .


46
Ponieważ pole bitowe jest zadeklarowane tak, jak intmyślę, może zawierać tylko wartości 0i -1.
Ozyrys

6
pomyśl tylko o tym, jak int przechowuje -1. Wszystkie bity są ustawione na 1. Stąd, jeśli masz tylko jeden bit, musi on wynosić -1. Czyli 1 i -1 w 1-bitowym int są takie same. Zmień zaznaczenie na „if (s.enabled! = 0)” i działa. Ponieważ 0 to nie może być.
Jürgen

3
Prawdą jest, że te zasady są takie same w C i C ++. Jednak zgodnie z zasadami użycia tagów powinniśmy oznaczyć to jako C i powstrzymać się od tagowania krzyżowego, gdy nie jest to potrzebne. Usunę część C ++, nie powinno to wpływać na żadne opublikowane odpowiedzi.
Lundin

8
Czy próbowałeś zmienić to na struct mystruct { unsigned int enabled:1; };?
ChatterOne,

4
Prosimy o zapoznanie się z zasadami dotyczącymi tagów C i C ++ , w szczególności z częścią dotyczącą krzyżowego tagowania C i C ++, ustaloną tutaj w drodze konsensusu społeczności . Nie zamierzam wdawać się w jakąś wojnę wycofywania, ale to pytanie jest niepoprawnie oznaczone C ++. Nawet jeśli zdarza się, że języki mają niewielką różnicę z powodu różnych TC, zadaj osobne pytanie o różnicę między C i C ++.
Lundin,

Odpowiedzi:


78

Pola bitowe są niezwykle słabo zdefiniowane przez standard. Biorąc pod uwagę ten kod struct mystruct {int enabled:1;};, nie wiemy:

  • Ile miejsca to zajmuje - czy są bity / bajty wypełnienia i gdzie znajdują się w pamięci.
  • Gdzie bit znajduje się w pamięci. Nieokreślony i zależy również od endii.
  • Czy pole int:nbitowe ma być traktowane jako podpisane, czy nie.

Odnośnie ostatniej części, C17 6.7.2.1/10 mówi:

Pole bitowe jest interpretowane jako mające typ liczby całkowitej ze znakiem lub bez znaku, składający się z określonej liczby bitów 125)

Nota nienormatywna wyjaśniająca powyższe:

125) Jak określono w 6.7.2 powyżej, jeśli faktycznym używanym specyfikatorem typu jest intlub nazwa-typu zdefiniowana jako int, to jest zdefiniowane w implementacji, czy pole bitowe jest podpisane, czy nie.

W przypadku, gdy pole bitowe ma być traktowane jako signed inti zrobisz trochę rozmiaru 1, to nie ma miejsca na dane, tylko na bit znaku. To jest powód, dla którego twój program może dawać dziwne wyniki na niektórych kompilatorach.

Dobra praktyka:

  • Nigdy nie używaj pól bitowych do jakichkolwiek celów.
  • Unikaj używania inttypu ze znakiem do jakiejkolwiek formy manipulacji bitami.

5
W pracy mamy static_asserts dotyczące rozmiaru i adresu pól bitowych, aby upewnić się, że nie są one wypełnione. Używamy pól bitowych dla rejestrów sprzętu w naszym oprogramowaniu.
Michael

4
@Lundin: Brzydka rzecz z # define-d maskami i offsetami polega na tym, że twój kod jest zaśmiecony przesunięciami i bitowymi operatorami AND / OR. W przypadku pól bitowych kompilator zajmie się tym za Ciebie.
Michael,

4
@Michael Dzięki bitfields kompilator zajmie się tym za Ciebie. Cóż, nie ma nic złego, jeśli twoje standardy „załatwiają to” są „nieprzenośne” i „nieprzewidywalne”. Moje są wyższe niż to.
Andrew Henle,

3
@AndrewHenle Leushenko mówi, że z perspektywy samego standardu C , to od implementacji zależy, czy zdecyduje się ona podążać za ABI x86-64, czy nie.
mtraceur,

3
@AndrewHenle Racja, zgadzam się w obu punktach. Chodziło mi o to, że myślę, że twój spór z Leushenko sprowadza się do tego, że używasz terminu „implementacja zdefiniowana” w odniesieniu tylko do rzeczy, które nie są ani ściśle określone przez standard C, ani ściśle określone przez platformę ABI, a on używa go do odniesienia do niczego, co nie jest ściśle określone tylko przez standard C.
mtraceur,

58

Nie jestem w stanie pojąć, jak to możliwe, że coś ustawiliśmy, a potem nie pojawia się tak, jak jest.

Czy pytasz, dlaczego kompiluje, a nie wyświetla błąd?

Tak, w idealnym przypadku powinien spowodować błąd. I tak jest, jeśli używasz ostrzeżeń kompilatora. W GCC z -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;
           ^

Powód, dla którego pozostawia się to do zdefiniowania implementacji w porównaniu z błędem, może mieć więcej wspólnego z historycznymi zastosowaniami, gdzie wymaganie rzutowania oznaczałoby złamanie starego kodu. Twórcy standardu mogą uważać, że ostrzeżenia były wystarczające, aby poprawić sytuację zainteresowanych.

Aby dorzucić nieco preskryptywizmu, powtórzę stwierdzenie @ Lundin: „Nigdy nie używaj pól bitowych do jakichkolwiek celów”. Jeśli masz dobre powody, aby uzyskać niskopoziomowe i szczegółowe szczegóły układu pamięci, które skłoniłyby cię do myślenia, że ​​potrzebujesz pól bitowych w pierwszej kolejności, inne powiązane wymagania, które prawie na pewno masz, napotkają na ich niedostateczną specyfikację.

(TL; DR - jeśli jesteś na tyle zaawansowany, że słusznie „potrzebujesz” pól bitowych, nie są one wystarczająco dobrze zdefiniowane, aby Ci służyć).


15
Autorzy standardu przebywali w święta w dniu, w którym projektowano rozdział o polu bitowym. Więc woźny musiał to zrobić. Nie ma uzasadnienie o czymkolwiek dotyczącym jak bit-pola są zaprojektowane.
Lundin

9
Nie ma spójnego uzasadnienia technicznego . Ale to prowadzi mnie do wniosku, że było polityczne uzasadnienie: aby uniknąć niepoprawności któregokolwiek z istniejących kodów lub implementacji. Ale w rezultacie niewiele jest w polach bitowych, na których można polegać.
John Bollinger,

6
@JohnBollinger Zdecydowanie obowiązywała polityka, która spowodowała wiele szkód w C90. Rozmawiałem kiedyś z członkiem komitetu, który wyjaśnił źródło wielu bzdur - norma ISO nie może faworyzować niektórych istniejących technologii. To dlatego utknęliśmy z kretyńskimi rzeczami, takimi jak obsługa dopełnienia 1 i wielkości ze znakiem char, sygnatura zdefiniowana przez implementację , obsługa bajtów, które nie mają 8 bitów itd., Itd. Nie wolno im było sprawiać, że komputery kretyńskie miały niekorzystny wpływ na rynek.
Lundin

1
@Lundin Ciekawie byłoby zobaczyć zbiór zapisów i sekcji zwłok od osób, które uważały, że kompromisy zostały popełnione przez pomyłkę, i dlaczego. Zastanawiam się, jak wiele badań tego, że „zrobiliśmy to ostatnim razem i wyszło / nie wyszło” stało się instytucjonalną wiedzą informującą o kolejnym takim przypadku, a nie tylko historiami w głowach ludzi.
HostileFork mówi, że nie ufaj SE

1
Jest to nadal wymienione jako punkt nr. 1 z pierwotnych zasad C w Karcie C2x: „Istniejący kod jest ważny, a istniejące implementacje nie”. ... „żadna implementacja nie była uważana za wzór, na podstawie którego można zdefiniować C: Zakłada się, że wszystkie istniejące implementacje muszą się nieco zmienić, aby były zgodne ze Standardem”.
Leushenko

23

Jest to zachowanie zdefiniowane w ramach implementacji. Zakładam, że maszyny, na których to uruchamiasz, używają liczb całkowitych ze intznakiem komplementu i traktują w tym przypadku jako liczbę całkowitą ze znakiem, aby wyjaśnić, dlaczego nie wpisujesz prawdziwej części instrukcji if.

struct mystruct { int enabled:1; };

deklaruje enablejako 1-bitowe pole bitowe. Ponieważ jest podpisany, prawidłowe wartości to -1i 0. Ustawienie pola na 1przepełnienie tego bitu wraca do -1(jest to niezdefiniowane zachowanie)

Zasadniczo, gdy mamy do czynienia z polem bitowym ze znakiem, maksymalna wartość 2^(bits - 1) - 1jest 0w tym przypadku.


„Ponieważ jest podpisany, prawidłowe wartości to -1 i 0”. Kto powiedział, że jest podpisany? To nie jest zdefiniowane, ale zachowanie zdefiniowane w implementacji. Jeśli jest podpisany, prawidłowe wartości to -i +. Dopełnienie 2 nie ma znaczenia.
Lundin,

5
@Lundin 1-bitowa liczba komplementów dwójek ma tylko dwie możliwe wartości. Jeśli bit jest ustawiony, to ponieważ jest to bit znaku, wynosi -1. Jeśli nie jest ustawiony, to jest „dodatni” 0. Wiem, że to jest zdefiniowana implementacja, tylko wyjaśniam wyniki przy użyciu najpopularniejszej implantacji
NathanOliver,

1
Kluczem jest raczej to, że uzupełnienie 2 lub jakakolwiek inna podpisana forma nie może działać z jednym dostępnym bitem.
Lundin

1
@JohnBollinger Rozumiem to. Dlatego mam discliamer, że jest to zdefiniowana implementacja. Przynajmniej w przypadku wielkiej trójki wszyscy traktują intw tym przypadku jako podpisane. Szkoda, że ​​pola bitowe są tak niedokładne. Zasadniczo jest to ta funkcja, skonsultuj się z kompilatorem, jak jej używać.
NathanOliver

1
@Lundin, sformułowanie standardu dotyczące reprezentacji liczb całkowitych ze znakiem doskonale radzi sobie w przypadku, gdy istnieją bity o zerowej wartości, przynajmniej w dwóch z trzech dozwolonych alternatyw. Działa to, ponieważ przypisuje (ujemne) wartości miejsca do bitów znaku, zamiast nadawać im algorytmiczną interpretację.
John Bollinger,

10

Można o tym myśleć jako o tym, że w systemie dopełnienia dwójki skrajny lewy bit jest bitem znaku. Każda liczba całkowita ze znakiem z ustawionym najbardziej po lewej stronie bitem jest zatem wartością ujemną.

Jeśli masz 1-bitową liczbę całkowitą ze znakiem, ma tylko bit znaku. A więc przypisywanie1 do tego pojedynczego bitu może ustawić tylko bit znaku. Tak więc, podczas czytania z powrotem, wartość jest interpretowana jako ujemna, a więc wynosi -1.

Wartości, które może przechowywać 1-bitowa liczba całkowita ze znakiem, to -2^(n-1)= -2^(1-1)= -2^0= -1i2^n-1= 2^1-1=0


8

Zgodnie ze standardem C ++ n4713 , dostępny jest bardzo podobny fragment kodu. Użyty typ to BOOL(niestandardowy), ale można go zastosować do dowolnego typu.

12.2.4

4 Jeżeli wartość prawda lub fałsz jest przechowywana w polu bitowymbooldowolnego typu (w tym jednobitowym polu bitowym), pierwotnaboolwartość i wartość pola bitowego są porównywane. Jeśli wartość modułu wyliczającego jest przechowywana w polu bitowym tego samego typu wyliczenia, a liczba bitów w polu bitowym jest wystarczająco duża, aby pomieścić wszystkie wartości tego typu wyliczenia (10.2), oryginalna wartość modułu wyliczającego i wartość wartość pola bitowego powinna być równa . [Przykład:

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

- przykład końca]


Na pierwszy rzut oka pogrubiona część wydaje się otwarta do interpretacji. Jednak poprawna intencja staje się jasna, gdy enum BOOLpochodzi z 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");
}

Z powyższym kodem daje ostrzeżenie bez -Wall -pedantic:

ostrzeżenie: „mystruct :: enabled” jest za mała, aby pomieścić wszystkie wartości „enum BOOL” struct mystruct { BOOL enabled:1; };

Wynik to:

Jest niepełnosprawny !! (podczas używania enum BOOL : int)

Jeśli enum BOOL : intjest uproszczony enum BOOL, to wyjście jest takie, jak określa powyższy standardowy fragment:

Jest włączony (podczas używania enum BOOL)


W związku z tym można wywnioskować, podobnie jak niewiele innych odpowiedzi, że inttyp ten nie jest wystarczająco duży, aby przechowywać wartość „1” w pojedynczym polu bitowym.


0

Nie ma nic złego w twoim rozumieniu pól bitowych, które widzę. Widzę, że przedefiniowałeś mystruct najpierw jako struct mystruct {int enabled: 1; } a następnie jako struct mystruct s; . Powinieneś był zakodować:

#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");
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.