Co jest ":-!!" w kodzie C?

1665

Wpadłem na ten dziwny kod makra w /usr/include/linux/kernel.h :

/* Force a compilation error if condition is true, but also produce a
   result (of value 0 and type size_t), so the expression can be used
   e.g. in a structure initializer (or where-ever else comma expressions
   aren't permitted). */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

Co ma :-!!zrobić?

chmurli
źródło
2
- Unary minus <br />! Logiczne NIE <br /> odwrotne nie nie dla podanej liczby całkowitej e, więc zmienna może wynosić 0 lub 1.
CyrillC
69
git blame mówi nam, że ta szczególna forma statycznego twierdzenia została wprowadzona przez Jana Beulicha w 8c87df4 . Oczywiście miał dobre powody, aby to zrobić (patrz komunikat zatwierdzenia).
Niklas B.,
55
@Lundin: assert () NIE powoduje błędu czasu kompilacji. To jest sedno powyższej konstrukcji.
Chris Pacejo
4
@GreweKokkor Nie bądź naiwny, Linux jest zbyt duży, aby jedna osoba poradziła sobie z tym wszystkim. Linus ma swoich poruczników i mają swoich, którzy popychają zmiany i ulepszenia od podstaw. Linus decyduje tylko, czy chce się wyróżniać, czy nie, ale do pewnego stopnia ufa współpracownikom. Jeśli chcesz dowiedzieć się więcej o tym, jak działa system rozproszony w środowisku open source, sprawdź wideo na youtube: youtube.com/watch?v=4XpnKHJAok8 (To bardzo interesująca rozmowa).
Tomas Pruzina
3
@ccloud, sizeof„ocenia” typ, ale nie wartość. Jest to typ nieprawidłowy w tym przypadku.
Winston Ewert

Odpowiedzi:

1692

Jest to w efekcie sposób sprawdzenia, czy wyrażenie e może być ocenione na 0, a jeśli nie, niepowodzenie kompilacji .

Makro jest nieco źle nazwane; powinno to być coś bardziej podobnego BUILD_BUG_OR_ZEROniż ...ON_ZERO. (Od czasu do czasu dyskutowano o tym, czy to mylące imię ).

Powinieneś przeczytać takie wyrażenie:

sizeof(struct { int: -!!(e); }))
  1. (e): Oblicz wyrażenie e.

  2. !!(e): Logicznie zaneguj dwukrotnie: 0if e == 0; inaczej 1.

  3. -!!(e): Zaneguj liczbowo wyrażenie z kroku 2: 0jeśli było 0; inaczej -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Jeśli było zero, deklarujemy strukturę z anonimowym całkowitym polem bitowym o szerokości zero. Wszystko jest w porządku i postępujemy jak zwykle.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Z drugiej strony, jeśli nie jest to zero, to będzie to pewna liczba ujemna. Zadeklarowanie dowolnego pola bitowego o ujemnej szerokości jest błędem kompilacji.

Więc albo skończymy z bitem o szerokości 0 w strukturze, co jest w porządku, albo z bitem o ujemnej szerokości, co jest błędem kompilacji. Następnie bierzemy sizeofto pole, więc otrzymujemy size_todpowiednią szerokość (która będzie wynosić zero w przypadku, gdy ewynosi zero).


Niektóre osoby pytają: dlaczego po prostu nie użyć assert?

Odpowiedź keithmo tutaj ma dobrą odpowiedź:

Te makra implementują test w czasie kompilacji, podczas gdy assert () jest testem w czasie wykonywania.

Dokładnie tak. Nie chcesz wykrywać problemów w jądrze w czasie wykonywania, które mogły zostać wcześniej wykryte ! To kluczowy element systemu operacyjnego. W jakimkolwiek stopniu problemy można wykryć w czasie kompilacji, tym lepiej.

John Feminella
źródło
5
@weston Wiele różnych miejsc. Sam zobacz!
John Feminella,
166
ostatnie warianty standardów C ++ lub C mają coś podobnego static_assertdo podobnych celów.
Basile Starynkevitch
54
@Lundin - #error wymagałby użycia 3 wierszy kodu # if / # error / # endif i działałby tylko w przypadku ocen dostępnych dla preprocesora. Ten hack działa dla każdej oceny dostępnej dla kompilatora.
Ed Staub,
236
Jądro Linuksa nie używa C ++, przynajmniej nie kiedy Linus wciąż żyje.
Mark Ransom,
6
@ Dolda2000: „ Wyrażenia boolowskie w C są zdefiniowane tak, aby zawsze miały wartość zero lub jeden ” - Niezupełnie. W operatorów , które dają „logicznie wartość logiczna” Wyniki ( !, <, >, <=, >=, ==, !=, &&, ||) zawsze dają wartość 0 lub 1. Inne wyrażenia może dać wyniki, które mogą być stosowane jako warunkach, ale jedynie zero lub niezerowe; na przykład, isdigit(c)gdzie cjest cyfrą, może dać dowolną niezerową wartość (która jest następnie traktowana jako prawdziwa w warunku).
Keith Thompson
256

To :jest pole bitowe. Jeśli chodzi o !!, to jest logiczne podwójna negacja i tak wraca 0do fałszywych lub 1za prawdziwe. A -to znak minus, tzn. Negacja arytmetyczna.

To tylko sztuczka, aby kompilator przestał działać przy nieprawidłowych danych wejściowych.

Zastanów się BUILD_BUG_ON_ZERO. Gdy zostanie -!!(e)oszacowana na wartość ujemną, spowoduje to błąd kompilacji. W przeciwnym razie -!!(e)wartość jest równa 0, a pole bitowe o szerokości 0 ma rozmiar 0. A zatem makro size_tma wartość 0.

Moim zdaniem nazwa jest słaba, ponieważ kompilacja faktycznie kończy się niepowodzeniem, gdy dane wejściowe nie są równe zero.

BUILD_BUG_ON_NULLjest bardzo podobny, ale daje wskaźnik zamiast an int.

David Heffernan
źródło
14
jest sizeof(struct { int:0; })ściśle zgodny?
ouah
7
Dlaczego generalnie wynik miałby być 0? A structz pustym polem bitowym, to prawda, ale nie sądzę, że struktura o rozmiarze 0 jest dozwolona. Np. Jeśli utworzyłbyś tablicę tego typu, poszczególne elementy tablicy wciąż muszą mieć różne adresy, nie?
Jens Gustedt,
2
tak naprawdę nie dbają o to, ponieważ używają rozszerzeń GNU, wyłączają ścisłą regułę aliasingu i nie uważają przelewów liczb całkowitych za UB. Zastanawiałem się jednak, czy jest to zgodne z C.
ouah,
3
@ oah dotyczące nienazwanych pól bitowych o zerowej długości, patrz tutaj: stackoverflow.com/questions/4297095/…
David Heffernan
9
@DavidHeffernan faktycznie C pozwala na nieograniczone pole bitów o 0szerokości, ale nie, jeśli w strukturze nie ma żadnego innego nazwanego elementu. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Na przykład sizeof (struct {int a:1; int:0;})jest ściśle zgodny, ale sizeof(struct { int:0; })nie jest (zachowanie nieokreślone).
ouah
168

Wydaje się, że niektórzy mylą te makra assert().

Te makra implementują test czasu kompilacji, podczas gdy assert()jest to test czasu wykonywania.

keithmo
źródło
52

Cóż, jestem dość zaskoczony, że nie wspomniano o alternatywach dla tej składni. Innym powszechnym (ale starszym) mechanizmem jest wywoływanie funkcji, która nie została zdefiniowana i poleganie na optymalizatorze w celu skompilowania wywołania funkcji, jeśli twoje twierdzenie jest poprawne.

#define MY_COMPILETIME_ASSERT(test)              \
    do {                                         \
        extern void you_did_something_bad(void); \
        if (!(test))                             \
            you_did_something_bad(void);         \
    } while (0)

Chociaż ten mechanizm działa (o ile włączone są optymalizacje), jego wadą jest nie zgłaszanie błędu do momentu połączenia, w którym to czasie nie można znaleźć definicji funkcji you_did_something_bad (). Dlatego programiści jądra zaczynają używać sztuczek, takich jak szerokości pola bitowego o ujemnej wielkości i tablice o rozmiarze ujemnym (późniejsze przestały łamać kompilacje w GCC 4.4).

W odpowiedzi na potrzebę asercji w czasie kompilacji, GCC 4.3 wprowadził erroratrybut funkcji, który pozwala rozwinąć tę starszą koncepcję, ale generuje błąd czasu kompilacji z wybranym przez Ciebie komunikatem - nigdy więcej tajemniczej „ujemnej tablicy” „komunikaty o błędach!

#define MAKE_SURE_THIS_IS_FIVE(number)                          \
    do {                                                        \
        extern void this_isnt_five(void) __attribute__((error(  \
                "I asked for five and you gave me " #number))); \
        if ((number) != 5)                                      \
            this_isnt_five();                                   \
    } while (0)

W rzeczywistości, począwszy od Linuksa 3.9, mamy teraz wywoływane makro, compiletime_assertktóre korzysta z tej funkcji, a większość makr bug.hzostała odpowiednio zaktualizowana. Tego makra nie można jednak używać jako inicjalizatora. Jednak używając wyrażeń instrukcji (inne rozszerzenie C GCC), możesz!

#define ANY_NUMBER_BUT_FIVE(number)                           \
    ({                                                        \
        typeof(number) n = (number);                          \
        extern void this_number_is_five(void) __attribute__(( \
                error("I told you not to give me a five!"))); \
        if (n == 5)                                           \
            this_number_is_five();                            \
        n;                                                    \
    })

To makro dokona oceny swojego parametru dokładnie raz (w przypadku, gdy ma skutki uboczne) i utworzy błąd kompilacji, który mówi: „Mówiłem, żebyś nie dawał mi pięciu!” jeśli wyrażenie ma wartość pięć lub nie jest stałą czasu kompilacji.

Dlaczego więc nie używamy tego zamiast ujemnych pól bitowych? Niestety, istnieje obecnie wiele ograniczeń użycia wyrażeń instrukcji, w tym ich użycia jako stałych inicjatorów (dla stałych wyliczeniowych, szerokości pola bitowego itp.), Nawet jeśli wyrażenie instrukcji jest całkowicie stałe, to znaczy, że można je w pełni ocenić w czasie kompilacji i w inny sposób przechodzi __builtin_constant_p()test). Ponadto nie można ich używać poza ciałem funkcji.

Mamy nadzieję, że GCC niedługo poprawi te niedociągnięcia i zezwoli na stosowanie stałych wyrażeń instrukcji jako stałych inicjatorów. Wyzwaniem jest tutaj specyfikacja języka określająca, co jest stałym wyrażeniem prawnym. C ++ 11 dodał słowo kluczowe constexpr tylko dla tego typu lub rzeczy, ale w C11 nie istnieje odpowiednik. Podczas gdy C11 otrzymał statyczne twierdzenia, które rozwiążą część tego problemu, nie rozwiąże wszystkich tych niedociągnięć. Mam więc nadzieję, że gcc może udostępnić funkcjonalność constexpr jako rozszerzenie poprzez -std = gnuc99 i -std = gnuc11 lub inne, i umożliwić jej użycie w wyrażeniach instrukcji et. glin.

Daniel Santos
źródło
6
Wszystkie twoje rozwiązania NIE są alternatywami. Komentarz nad makrem jest dość jasny. ” so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted).„ Makro zwraca wyrażenie typusize_t
Wiz
3
@Wiz Tak, jestem tego świadomy. Być może było to trochę zbyt szczegółowe i może muszę ponownie odwiedzić moje sformułowania, ale moim celem było zbadanie różnych mechanizmów statycznych twierdzeń i wykazanie, dlaczego nadal używamy pól bitowych o ujemnej wielkości. Krótko mówiąc, jeśli otrzymamy mechanizm ciągłego wyrażania instrukcji, będziemy mieli inne opcje otwarte.
Daniel Santos,
W każdym razie nie możemy użyć tych makr do zmiennej. dobrze? error: bit-field ‘<anonymous>’ width not an integer constantPozwala to tylko na stałe. Więc jaki jest pożytek?
Karthik Raj Palanichamy
1
@Karthik Przeszukaj źródła jądra Linux, aby zobaczyć, dlaczego jest ono używane.
Daniel Santos,
@ superuper Nie rozumiem, w jaki sposób twój komentarz jest w ogóle powiązany. Czy możesz to poprawić, lepiej wyjaśnić, co masz na myśli, lub usunąć?
Daniel Santos,
36

Tworzy pole 0bitowe wielkości, jeśli warunek jest fałszywy, ale pole bitowe wielkości -1( -!!1), jeśli warunek jest prawdziwy / niezerowy. W pierwszym przypadku nie ma błędu, a struktura jest inicjowana za pomocą elementu int. W tym drugim przypadku występuje błąd kompilacji (i -1oczywiście nie powstaje coś takiego jak pole bitowe wielkości ).

Matt Phillips
źródło
3
W rzeczywistości zwraca size_twartość 0, jeśli warunek jest spełniony.
David Heffernan