tutorial - programming in c pdf




Was ist ":-!!" in C-Code? (4)

Ich stieß auf diesen merkwürdigen /usr/include/linux/kernel.h in /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); }))

Was macht :-!! machen?


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

Es ist alles nur ein Trick, um den Compiler bei ungültigen Eingaben zu blockieren.

Betrachten Sie BUILD_BUG_ON_ZERO . Wenn -!!(e) zu einem negativen Wert führt, entsteht ein Kompilierungsfehler. Sonst -!!(e) ergibt 0, und ein Bitfeld mit 0 Breite hat die Größe 0. Daher wird das Makro zu einer size_t mit dem Wert 0 ausgewertet.

Der Name ist meiner Meinung nach schwach, weil der Build tatsächlich fehlschlägt, wenn die Eingabe nicht null ist.

BUILD_BUG_ON_NULL ist sehr ähnlich, liefert aber einen Zeiger anstelle eines int .


Dies ist in der Tat eine Möglichkeit, um zu überprüfen, ob der Ausdruck e als 0 bewertet werden kann und wenn nicht, um den Build zu versagen .

Das Makro ist etwas falsch benannt; Es sollte etwas mehr wie BUILD_BUG_OR_ZERO , anstatt ...ON_ZERO . (Es gab gelegentlich Diskussionen darüber, ob dies ein verwirrender Name ist .)

Du solltest den Ausdruck so lesen:

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

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

  3. -!!(e) : Negiere den Ausdruck von Schritt 2: 0 wenn er 0 ; sonst -1 .

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

  5. struct{int: -!!(1);} --> struct{int: -1;} : Andererseits, wenn es nicht Null ist, dann wird es eine negative Zahl sein. Das Deklarieren eines Bitfeldes mit negativer Breite ist ein Kompilierungsfehler.

Also werden wir 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 die sizeof dieses Feldes, so erhalten wir eine size_t mit der entsprechenden Breite (die für den Fall, dass e 0 ist, Null ist).

Einige Leute haben gefragt: Warum nicht einfach eine assert ?

keithmos Antwort hat hier eine gute Antwort:

Diese Makros implementieren einen Kompilierungszeittest, während assert () ein Laufzeittest ist.

Genau richtig. Sie möchten zur Laufzeit keine Probleme in Ihrem Kernel entdecken, die früher hätten entdeckt werden können! Es ist ein kritischer Teil des Betriebssystems. In welchem ​​Umfang auch immer Probleme zur Kompilierzeit erkannt werden können, umso besser.


Es erzeugt ein Bitfeld der Größe 0 wenn die Bedingung falsch ist, aber ein Bitfeld der Größe -1 ( -!!1 ), wenn die Bedingung wahr / ungleich Null ist. Im ersten Fall gibt es keinen Fehler und die Struktur wird mit einem int-Member initialisiert. Im letzteren Fall gibt es einen Kompilierfehler (und natürlich wird kein Bitfeld der Grße -1 erzeugt).


Nun, ich bin ziemlich überrascht, dass die Alternativen zu dieser Syntax nicht erwähnt wurden. Ein weiterer häufiger (aber älterer) Mechanismus besteht darin, eine Funktion aufzurufen, die nicht definiert ist, und sich darauf zu verlassen, dass der Optimierer den Funktionsaufruf kompiliert, wenn Ihre Assertion 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 dann einen Fehler meldet, wenn Sie ihn verlinken. Zu diesem Zeitpunkt findet er die Definition für die Funktion you_did_something_bad () nicht. Das ist der Grund, warum Kernel-Entwickler Tricks wie die Bitfeldbreiten der negativen Größe und die Arrays negativer Größe (die später in GCC 4.4 die Builds gestoppt haben) verwenden.

Aus Sympathie für die Notwendigkeit kompilierbarer Assertions hat GCC 4.3 das error eingeführt, das es erlaubt, dieses ältere Konzept zu erweitern, aber einen Kompilierungsfehler mit einer Nachricht Ihrer Wahl zu erzeugen - keine kryptische "negative Größe" mehr Array "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 in bug.h wurden entsprechend aktualisiert. Dennoch kann dieses Makro nicht als Initialisierer verwendet werden. Mit Anweisungsausdrücken (eine andere 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 wird seinen Parameter genau einmal auswerten (falls es Nebenwirkungen hat) und einen Kompilierungsfehler erstellen, der besagt "Ich habe dir gesagt, dass du mir keine fünf gibst!" wenn der Ausdruck fünf ergibt oder keine Kompilierzeitkonstante ist.

Warum verwenden wir das 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 Konstanteninitialisierer (für Enum-Konstanten, Bitfeldbreite usw.), selbst wenn der Anweisungsausdruck vollständig selbst konstant ist (dh vollständig ausgewertet werden kann) zur Kompilierzeit und übergibt ansonsten den Test __builtin_constant_p() ). Außerdem können sie nicht außerhalb eines Funktionskörpers verwendet werden.

Hoffentlich wird der GCC diese Mängel bald korrigieren und ermöglichen, dass konstante Anweisungsausdrücke als konstante Initialisierer verwendet werden. Die Herausforderung hier ist die Sprachspezifikation, die definiert, was ein gesetzlicher konstanter Ausdruck ist. C ++ 11 hat das Schlüsselwort constexpr nur für diesen Typ oder dieses Ding hinzugefügt, aber in C11 ist kein Gegenstück vorhanden. Während C11 statische Behauptungen erhielt, die einen Teil dieses Problems lösen werden, wird es nicht alle diese Mängel lösen. Also hoffe ich, dass gcc eine conexpr-Funktionalität als Erweiterung über -std = gnuc99 & -std = gnuc11 oder etwas ähnliches zur Verfügung stellen kann und seine Verwendung für Anweisungsausdrücke et. al.





linux-kernel