Was ist ":-!!" in C-Code?

1665

Ich bin auf diesen seltsamen Makrocode in /usr/include/linux/kernel.h gestoßen :

/* 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); }))

Was macht :-!!das

chmurli
quelle
2
- Unär minus <br />! Logisch NICHT <br /> invers nicht nicht von der angegebenen Ganzzahl e, so dass die Variable entweder 0 oder 1 sein kann.
CyrillC
69
Git Schuld sagt uns, dass diese besondere Form der statischen Behauptung von Jan Beulich in 8c87df4 eingeführt wurde . Offensichtlich hatte er gute Gründe dafür (siehe die Commit-Nachricht).
Niklas B.
55
@Lundin: assert () verursacht KEINEN Fehler bei der Kompilierung. Das ist der springende Punkt der obigen Konstruktion.
Chris Pacejo
4
@GreweKokkor Sei nicht naiv, Linux ist zu groß für eine Person, um alles zu erledigen. Linus hat seine Leutnants und sie haben ihre, die Veränderungen und Verbesserungen von unten nach oben vorantreiben. Linus entscheidet nur, ob er ein Feature will oder nicht, aber er vertraut seinen Mitarbeitern in gewissem Maße. Wenn Sie mehr über die Funktionsweise eines verteilten Systems in einer Open Source-Umgebung erfahren möchten, schauen Sie sich das YouTube-Video an: youtube.com/watch?v=4XpnKHJAok8 (Es ist ein sehr interessantes Gespräch).
Tomas Pruzina
3
@cpcloud sizeof"wertet" den Typ aus, nur nicht den Wert. Dies ist der Typ, der in diesem Fall ungültig ist.
Winston Ewert

Antworten:

1692

Auf diese Weise können Sie überprüfen, ob der Ausdruck e mit 0 bewertet werden kann, und wenn nicht, den Build fehlschlagen .

Das Makro ist etwas falsch benannt; es sollte eher so etwas sein BUILD_BUG_OR_ZEROals ...ON_ZERO. (Es gab gelegentlich Diskussionen darüber, ob dies ein verwirrender Name ist .)

Sie sollten den Ausdruck folgendermaßen lesen:

sizeof(struct { int: -!!(e); }))
  1. (e): Ausdruck berechnen e.

  2. !!(e): Zweimal logisch negieren: 0if e == 0; sonst 1.

  3. -!!(e): Negiere den Ausdruck aus Schritt 2 numerisch: 0wenn es war 0; sonst -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Wenn es Null war, deklarieren wir eine Struktur mit einem anonymen ganzzahligen Bitfeld mit der Breite Null. Alles ist in Ordnung und wir gehen wie gewohnt vor.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Wenn es andererseits nicht Null ist, ist es eine negative Zahl. Das Deklarieren eines Bitfelds mit negativer Breite ist ein Kompilierungsfehler.

Wir werden also entweder mit einem Bitfeld mit der Breite 0 in einer Struktur enden, was in Ordnung ist, oder mit einem Bitfeld mit negativer Breite, was ein Kompilierungsfehler ist. Dann nehmen wir sizeofdieses Feld, so dass wir ein size_tmit der entsprechenden Breite erhalten (die Null ist, wenn eNull ist).


Einige Leute haben gefragt: Warum nicht einfach eine verwenden assert?

Keithmos Antwort hier hat eine gute Antwort:

Diese Makros implementieren einen Test zur Kompilierungszeit, während assert () ein Laufzeit-Test ist.

Genau richtig. Sie möchten zur Laufzeit keine Probleme in Ihrem Kernel erkennen , die früher hätten erkannt werden können! Es ist ein kritischer Teil des Betriebssystems. Inwieweit Probleme beim Kompilieren erkannt werden können, umso besser.

John Feminella
quelle
5
@weston Viele verschiedene Orte. Überzeugen Sie sich selbst!
John Feminella
166
Neuere Varianten von C ++ - oder C-Standards haben etwas Ähnliches static_assertfür verwandte Zwecke.
Basile Starynkevitch
54
@Lundin - #error würde die Verwendung von 3 Codezeilen # if / # error / # endif erfordern und würde nur für Auswertungen funktionieren, auf die der Vorprozessor zugreifen kann. Dieser Hack funktioniert für jede Auswertung, auf die der Compiler zugreifen kann.
Ed Staub
236
Der Linux-Kernel verwendet kein C ++, zumindest nicht, solange Linus noch lebt.
Mark Ransom
6
@ Dolda2000: " Boolesche Ausdrücke in C werden so definiert, dass sie immer null oder eins ergeben " - Nicht genau. Die Betreiber , dass yield „logisch boolean“ Ergebnisse ( !, <, >, <=, >=, ==, !=, &&, ||) immer ergeben 0 oder 1. Andere Ausdrücke können Ergebnisse liefern , die als Bedingungen verwendet werden können, sondern nur Null oder ungleich Null; zum Beispiel isdigit(c), wo ceine Ziffer ist, kann ergeben beliebigen Nicht-Null - Wert (die dann als wahr in einem Zustand , der behandelt wird).
Keith Thompson
256

Das :ist ein Bitfeld. Das !!ist eine logische doppelte Negation und gibt daher 0für falsch oder 1für wahr zurück. Und das -ist ein Minuszeichen, dh eine arithmetische Negation.

Es ist alles nur ein Trick, um den Compiler dazu zu bringen, ungültige Eingaben zu sperren.

Überlegen Sie BUILD_BUG_ON_ZERO. Wenn -!!(e)ein negativer Wert ausgewertet wird, entsteht ein Kompilierungsfehler. Andernfalls wird -!!(e)0 ausgewertet, und ein Bitfeld mit einer Breite von 0 hat die Größe 0. Daher wird das Makro size_tmit dem Wert 0 a ausgewertet .

Der Name ist meiner Ansicht nach schwach, da der Build tatsächlich fehlschlägt, wenn die Eingabe nicht Null ist.

BUILD_BUG_ON_NULList sehr ähnlich, liefert aber eher einen Zeiger als einen int.

David Heffernan
quelle
14
ist sizeof(struct { int:0; })streng konform?
Ouah
7
Warum sollte das Ergebnis im Allgemeinen sein 0? A structmit nur einem leeren Bitfeld, stimmt, aber ich denke nicht, dass Strukturen mit der Größe 0 erlaubt sind. Wenn Sie beispielsweise ein Array dieses Typs erstellen möchten, müssen die einzelnen Array-Elemente immer noch unterschiedliche Adressen haben, nicht wahr?
Jens Gustedt
2
Es ist ihnen eigentlich egal, da sie GNU-Erweiterungen verwenden, sie deaktivieren die strikte Aliasing-Regel und betrachten Ganzzahlüberläufe nicht als UB. Aber ich habe mich gefragt, ob dies genau C.
ouah
3
@ouah in Bezug auf unbenannte Bitfelder mit der Länge Null, siehe hier: stackoverflow.com/questions/4297095/…
David Heffernan
9
@DavidHeffernan C erlaubt tatsächlich ein unbenanntes Bitfeld der 0Breite, aber nicht, wenn es kein anderes benanntes Mitglied in der Struktur gibt. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."So ist zum Beispiel sizeof (struct {int a:1; int:0;})streng konform, aber sizeof(struct { int:0; })nicht (undefiniertes Verhalten).
Ouah
168

Einige Leute scheinen diese Makros mit zu verwechseln assert().

Diese Makros implementieren einen Test zur Kompilierungszeit, während assert()es sich um einen Laufzeit-Test handelt.

Keithmo
quelle
52

Nun, ich bin ziemlich überrascht, dass die Alternativen zu dieser Syntax nicht erwähnt wurden. Ein weiterer gängiger (aber älterer) Mechanismus besteht darin, eine nicht definierte Funktion aufzurufen und sich beim Kompilieren des Funktionsaufrufs auf den Optimierer zu verlassen, wenn Ihre Behauptung korrekt ist.

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

Während dieser Mechanismus funktioniert (solange Optimierungen aktiviert sind), hat er den Nachteil, dass er erst beim Verknüpfen einen Fehler meldet. Zu diesem Zeitpunkt findet er die Definition für die Funktion you_did_something_bad () nicht. Aus diesem Grund beginnen Kernel-Entwickler mit Tricks wie den Bitfeldbreiten mit negativer Größe und den Arrays mit negativer Größe (von denen die späteren Builds in GCC 4.4 nicht mehr brechen).

In Sympathie für die Notwendigkeit von Zusicherungen zur Kompilierungszeit hat GCC 4.3 das errorFunktionsattribut eingeführt , mit dem Sie dieses ältere Konzept erweitern können, aber einen Fehler zur Kompilierungszeit mit einer Nachricht Ihrer Wahl generieren können - kein kryptisches Array mit negativer Größe mehr " Fehlermeldungen!

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

Tatsächlich haben wir ab Linux 3.9 ein Makro namens compiletime_assert, das diese Funktion verwendet, und die meisten Makros bug.hwurden entsprechend aktualisiert. Dieses Makro kann jedoch nicht als Initialisierer verwendet werden. Mit Anweisungsausdrücken (einer anderen GCC C-Erweiterung) können Sie jedoch!

#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;                                                    \
    })

Dieses Makro wertet seinen Parameter genau einmal aus (falls es Nebenwirkungen hat) und erstellt einen Fehler bei der Kompilierung, der besagt: "Ich habe dir gesagt, du sollst mir keine Fünf geben!" wenn der Ausdruck fünf ergibt oder keine Kompilierungszeitkonstante ist.

Warum verwenden wir dies nicht anstelle von Bitfeldern mit negativer Größe? Leider gibt es derzeit viele Einschränkungen bei der Verwendung von Anweisungsausdrücken, einschließlich ihrer Verwendung als konstante Initialisierer (für Enum-Konstanten, Bitfeldbreite usw.), selbst wenn der Anweisungsausdruck selbst vollständig konstant ist (dh vollständig ausgewertet werden kann) zur Kompilierungszeit und besteht ansonsten den __builtin_constant_p()Test). Außerdem können sie nicht außerhalb eines Funktionskörpers verwendet werden.

Hoffentlich wird GCC diese Mängel bald ändern und die Verwendung konstanter Anweisungsausdrücke als konstante Initialisierer ermöglichen. Die Herausforderung hierbei ist die Sprachspezifikation, die definiert, was ein rechtlicher konstanter Ausdruck ist. C ++ 11 hat das Schlüsselwort constexpr nur für diesen Typ oder dieses Objekt hinzugefügt, aber in C11 ist kein Gegenstück vorhanden. Während C11 statische Zusicherungen erhalten hat, die einen Teil dieses Problems lösen, werden nicht alle diese Mängel behoben. Daher hoffe ich, dass gcc eine constexpr-Funktionalität als Erweiterung über -std = gnuc99 & -std = gnuc11 oder eine solche zur Verfügung stellen und deren Verwendung für Anweisungsausdrücke et ermöglichen kann. al.

Daniel Santos
quelle
6
Alle Ihre Lösungen sind KEINE Alternativen. Der Kommentar über dem Makro ist ziemlich klar. " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." Das Makro gibt einen Ausdruck vom Typ zurücksize_t
Wiz
3
@Wiz Ja, das ist mir bewusst. Vielleicht war das etwas ausführlich und vielleicht muss ich meinen Wortlaut noch einmal überprüfen, aber mein Ziel war es, die verschiedenen Mechanismen für statische Behauptungen zu untersuchen und zu zeigen, warum wir immer noch Bitfelder mit negativer Größe verwenden. Kurz gesagt, wenn wir einen Mechanismus für den Ausdruck konstanter Anweisungen erhalten, stehen andere Optionen offen.
Daniel Santos
Auf jeden Fall können wir dieses Makro nicht für eine Variable verwenden. Recht? error: bit-field ‘<anonymous>’ width not an integer constantEs erlaubt nur Konstanten. Also, was nützt das?
Karthik Raj Palanichamy
1
@Karthik Durchsuchen Sie die Quellen des Linux-Kernels, um herauszufinden, warum er verwendet wird.
Daniel Santos
@supercat Ich sehe nicht, wie dein Kommentar überhaupt zusammenhängt. Können Sie es bitte überarbeiten, besser erklären, was Sie meinen, oder es entfernen?
Daniel Santos
36

Es wird ein 0Größenbitfeld erstellt, wenn die Bedingung falsch ist, aber ein Größenbitfeld -1( -!!1), wenn die Bedingung wahr / ungleich Null ist. Im ersteren Fall liegt kein Fehler vor und die Struktur wird mit einem int-Member initialisiert. Im letzteren Fall liegt ein Kompilierungsfehler vor (und es wird -1natürlich kein Größenbitfeld erstellt).

Matt Phillips
quelle
3
Tatsächlich wird ein size_tWert mit dem Wert 0 zurückgegeben, falls die Bedingung erfüllt ist.
David Heffernan