Wie funktioniert die __is_constexpr(x)
Makro des Linux-Kernels funktionieren? Was ist seine Aufgabe? Wann wurde es eingeführt? Warum wurde es eingeführt?
/*
* This returns a constant expression while determining if an argument is
* a constant expression, most importantly without evaluating the argument.
* Glory to Martin Uecker <[email protected]>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Für ein Diskussion über verschiedene Ansätze Um das gleiche Problem zu lösen, lesen Sie stattdessen: Erkennung ganzzahliger Konstantenausdrücke in Makros
Die des Linux-Kernels __is_constexpr
Makro
Einführung
Das __is_constexpr(x)
Makro finden Sie im Linux-Kernel include/kernel/kernel.h:
/*
* This returns a constant expression while determining if an argument is
* a constant expression, most importantly without evaluating the argument.
* Glory to Martin Uecker <[email protected]>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Es wurde während des Zusammenführungsfensters für Linux Kernel v4.17 eingeführt, übertrage 3c8ba0d61d04 am 05.04.2018; obwohl die Diskussionen darüber einen Monat zuvor begannen.
Das Makro zeichnet sich dadurch aus, dass es subtile Details des C-Standards nutzt: die Bedingter Operatordie Regeln von zur Bestimmung des zurückgegebenen Typs (6.5.15.6) und die Definition von a Nullzeiger konstant (6.3.2.3.3).
Darüber hinaus stützt es sich auf sizeof(void)
erlaubt (und anders als sizeof(int)
), die ein GNU-C-Erweiterung.
Wie funktioniert es?
Der Körper des Makros ist:
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
Konzentrieren wir uns auf diesen Teil:
((void *)((long)(x) * 0l))
Beachten Sie das (long)(x)
Besetzung ist beabsichtigt erlauben x
Zeigertypen zu haben und Warnungen zu vermeiden u64
Typen auf 32-Bit-Plattformen. Dieses Detail ist jedoch für das Verständnis der Kernpunkte des Makros nicht wichtig.
Wenn x
ist ein ganzzahliger konstanter Ausdruck (6.6.6), dann folgt daraus ((long)(x) * 0l)
ist ein ganzzahliger konstanter Ausdruck von Wert 0
. Deswegen, (void *)((long)(x) * 0l)
ist ein Nullzeiger konstant (6.3.2.3.3):
Ein ganzzahliger konstanter Ausdruck mit dem Wert 0 oder ein solcher Ausdruck, der in einen Typ umgewandelt wird Leere *heißt a Nullzeiger konstant
Wenn x
ist nicht ein ganzzahliger konstanter Ausdruckdann (void *)((long)(x) * 0l)
ist kein Nullzeiger konstant, unabhängig von seinem Wert.
Wenn wir das wissen, können wir sehen, was danach passiert:
8 ? ((void *)((long)(x) * 0l)) : (int *)8
Hinweis: die zweite 8
wörtlich ist beabsichtigt um Compiler-Warnungen über das Erstellen von Zeigern auf nicht ausgerichtete Adressen zu vermeiden. Der Erste 8
wörtlich könnte einfach sein 1
. Diese Details sind jedoch für das Verständnis der Kernpunkte des Makros nicht wichtig.
Der Schlüssel hier ist, dass die Bedingter Operator gibt a zurück Anderer Typ abhängig davon, ob einer der Operanden a ist Nullzeiger konstant (6.5.15.6):
[…] wenn ein Operand eine Nullzeigerkonstante ist, hat das Ergebnis den Typ des anderen Operanden; andernfalls ist ein Operand ein Zeiger auf Leere oder eine qualifizierte Version von Leerein diesem Fall ist der Ergebnistyp ein Zeiger auf eine entsprechend qualifizierte Version von Leere.
Also, wenn x
war ein ganzzahliger konstanter Ausdruckdann ist der zweite Operand a Nullzeiger konstant und daher ist der Typ des Ausdrucks der Typ des dritten Operanden, der ein Zeiger auf ist int
.
Andernfallsder zweite Operand ist ein Zeiger auf void
und somit ist der Typ des Ausdrucks ein Zeiger auf void
.
Daher haben wir am Ende zwei Möglichkeiten:
sizeof(int) == sizeof(*((int *) (NULL))) // if `x` was an integer constant expression
sizeof(int) == sizeof(*((void *)(....))) // otherwise
Laut dem GNU-C-Erweiterung, sizeof(void) == 1
. Daher, wenn x
war ein ganzzahliger konstanter Ausdruckist das Ergebnis des Makros 1
; Andernfalls, 0
.
Da wir außerdem nur auf Gleichheit zwei vergleichen sizeof
Ausdrücke, das Ergebnis selbst ist ein anderes ganzzahliger konstanter Ausdruck (6.6.3, 6.6.6):
Konstante Ausdrücke dürfen keine Zuweisungs-, Inkrement-, Dekrement-, Funktionsaufruf- oder Kommaoperatoren enthalten, es sei denn, sie sind in einem Unterausdruck enthalten, der nicht ausgewertet wird.
Ein ganzzahliger konstanter Ausdruck muss einen ganzzahligen Typ haben und darf nur Operanden haben, die ganzzahlige Konstanten, Aufzählungskonstanten, Zeichenkonstanten, Größe von Ausdrücke, deren Ergebnisse ganzzahlige Konstanten sind, und Gleitkommakonstanten, die die unmittelbaren Operanden von Umwandlungen sind. Cast-Operatoren in einem ganzzahligen konstanten Ausdruck konvertieren nur arithmetische Typen in ganzzahlige Typen, außer als Teil eines Operanden für die Größe von Operator.
Zusammenfassend also die __is_constexpr(x)
Makro gibt ein zurück ganzzahliger konstanter Ausdruck von Wert 1
wenn das Argument ein ganzzahliger konstanter Ausdruck ist. Andernfalls wird ein zurückgegeben ganzzahliger konstanter Ausdruck von Wert 0
.
Warum wurde es eingeführt?
Das Makro ist entstanden während der Anstrengung alle zu entfernen Arrays mit variabler Länge (VLAs) aus dem Linux-Kernel.
Um dies zu erleichtern, war es wünschenswert, dies zu ermöglichen GCCs -Wvla
Warnung Kernel-weit; sodass alle Instanzen von VLAs vom Compiler gekennzeichnet wurden.
Als die Warnung aktiviert war, stellte sich heraus, dass GCC viele Fälle meldete, in denen Arrays VLAs waren, was nicht beabsichtigt war. Zum Beispiel im fs/btrfs/tree-checker.c:
#define BTRFS_NAME_LEN 255
#define XATTR_NAME_MAX 255
char namebuf[max(BTRFS_NAME_LEN, XATTR_NAME_MAX)];
Das darf ein Entwickler erwarten max(BTRFS_NAME_LEN, XATTR_NAME_MAX)
wurde beschlossen 255
und daher sollte es als Standard-Array (dh Nicht-VLA) behandelt werden. Dies hängt jedoch davon ab, was die max(x, y)
Makro erweitert zu.
Das Hauptproblem besteht darin, dass GCC VLA-Code generiert, wenn die Größe des Arrays keine ist (ganzzahliger) konstanter Ausdruck wie von der C-Norm definiert. Zum Beispiel:
#define not_really_constexpr ((void)0, 100)
int a[not_really_constexpr];
Gemäß dem C90-Standard, ((void)0, 100)
ist nicht a ständiger Ausdruck (6.6), aufgrund der Komma-Operator verwendet wird (6.6.3). In diesem Fall entscheidet sich GCC für die Ausgabe des VLA-Codes, selbst wenn es weiß, dass die Größe eine Kompilierzeitkonstante ist. Clang dagegen nicht.
Seit der max(x, y)
macro im Kernel kein konstanter Ausdruck war, löste GCC die Warnungen aus und generierte VLA-Code, wo Kernel-Entwickler es nicht beabsichtigt hatten.
Daher haben einige Kernel-Entwickler versucht, alternative Versionen des zu entwickeln max
und andere Makros, um die Warnungen und den VLA-Code zu vermeiden. Einige Versuche versuchten, Hebelwirkung zu erzielen GCCs __builtin_constant_p
eingebautaber kein Ansatz funktionierte mit allen Versionen von GCC, die der Kernel zu diesem Zeitpunkt unterstützte (gcc >= 4.4
).
Irgendwann Martin Uecker vorgeschlagen ein besonders cleverer Ansatz, der keine eingebauten Funktionen verwendet (sich inspirieren lassen aus glibcs tgmath.h):
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
Während der Ansatz eine GCC-Erweiterung verwendet, es kam trotzdem gut an und wurde als Schlüsselidee hinter dem verwendet __is_constexpr(x)
Makro, das nach einigen Iterationen mit anderen Entwicklern im Kernel auftauchte. Das Makro wurde dann verwendet, um das zu implementieren max
Makro und andere Makros, die konstante Ausdrücke sein müssen, um zu vermeiden, dass GCC VLA-Code generiert.
Ich glaube nicht, dass das allgemein bekannt ist
sizeof(void)
ist laut Norm nicht erlaubt… Die relevanten Passagen sind §6.5.3.4 Abs. 1 (void
ist ein unvollständiger Typ) und §6.2.5 Abs. 19 (keine unvollständigen Typen zusizeof
was offensichtlich sein sollte).– nemequ
25. März 2018 um 21:54 Uhr
Sie haben nicht klargestellt, dass “Erklären Sie ein Makro, das auf einen ganzzahligen konstanten Ausdruck testet” Ihre eigene aktuelle Frage war. Ist das von praktischem Nutzen oder akademischer Kaugummi? Schöne Antwort auf deine eigene Frage.
– Wetterfahne
25. März 2018 um 21:55 Uhr
@WeatherVane: Danke! Dieses Makro wurde vor ein paar Tagen in der LKML als mögliche Lösung für ein Problem mit VLAs im Kernel diskutiert, daher ist es klar, dass es einen praktischen Nutzen hat – besonders, wenn es im Kernelbaum landet.
– Eichel
25. März 2018 um 22:29 Uhr
Ich bin da bei Linus. Und da dieses Makro auch Compiler-spezifisch ist, warum nicht ein eingebautes verwenden, das klar wäre?
– zu ehrlich für diese Seite
26. März 2018 um 0:15 Uhr
Ich bin mir nicht sicher, wie jemand die Frage unklar findet
– MM
27. März 2018 um 20:43 Uhr