Warum tut dies .c
Datei #include
selbst?
vsimple.c
#define USIZE 8
#include "vsimple.c"
#undef USIZE
#define USIZE 16
#include "vsimple.c"
#undef USIZE
#define USIZE 32
#include "vsimple.c"
#undef USIZE
#define USIZE 64
#include "vsimple.c"
#undef USIZE
Die Datei enthält sich selbst, sodass derselbe Quellcode verwendet werden kann, um 4 verschiedene Funktionssätze für bestimmte Werte des Makros zu generieren USIZE
.
Das #include
Direktiven sind eigentlich in einer eingeschlossen #ifndef
was die Rekursion auf eine einzige Ebene beschränkt:
#ifndef USIZE
// common definitions
...
//
#define VSENC vsenc
#define VSDEC vsdec
#define USIZE 8
#include "vsimple.c"
#undef USIZE
#define USIZE 16
#include "vsimple.c"
#undef USIZE
#define USIZE 32
#include "vsimple.c"
#undef USIZE
#define USIZE 64
#include "vsimple.c"
#undef USIZE
#else // defined(USIZE)
// macro expanded size specific functions using token pasting
...
#define uint_t TEMPLATE3(uint, USIZE, _t)
unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) {
...
}
unsigned char *TEMPLATE2(VSDEC, USIZE)(unsigned char *__restrict ip, size_t n, uint_t *__restrict op) {
...
}
#endif
Die in diesem Modul definierten Funktionen sind
// vsencNN: compress array with n unsigned (NN bits in[n]) values to the buffer out. Return value = end of compressed output buffer out
unsigned char *vsenc8( unsigned char *__restrict in, size_t n, unsigned char *__restrict out);
unsigned char *vsenc16(unsigned short *__restrict in, size_t n, unsigned char *__restrict out);
unsigned char *vsenc32(unsigned *__restrict in, size_t n, unsigned char *__restrict out);
unsigned char *vsenc64(uint64_t *__restrict in, size_t n, unsigned char *__restrict out);
// vsdecNN: decompress buffer into an array of n unsigned values. Return value = end of compressed input buffer in
unsigned char *vsdec8( unsigned char *__restrict in, size_t n, unsigned char *__restrict out);
unsigned char *vsdec16(unsigned char *__restrict in, size_t n, unsigned short *__restrict out);
unsigned char *vsdec32(unsigned char *__restrict in, size_t n, unsigned *__restrict out);
unsigned char *vsdec64(unsigned char *__restrict in, size_t n, uint64_t *__restrict out);
Sie sind alle aus den beiden Funktionsdefinitionen in erweitert vsimple.c:
unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) {
...
}
unsigned char *TEMPLATE2(VSDEC, USIZE)(unsigned char *__restrict ip, size_t n, uint_t *__restrict op) {
...
}
Das TEMPLATE2
und TEMPLATE3
Makros sind definiert in conf.h wie
#define TEMPLATE2_(_x_, _y_) _x_##_y_
#define TEMPLATE2(_x_, _y_) TEMPLATE2_(_x_,_y_)
#define TEMPLATE3_(_x_,_y_,_z_) _x_##_y_##_z_
#define TEMPLATE3(_x_,_y_,_z_) TEMPLATE3_(_x_, _y_, _z_)
Diese Makros sind klassische Präprozessorkonstruktionen, um Bezeichner per Token-Einfügen zu erstellen. TEMPLATE2
und TEMPLATE2_
werden häufiger genannt GLUE
und XGLUE
.
Die Funktionsvorlage beginnt wie folgt:
unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) ...
Es wird bei der ersten rekursiven Inklusion mit expandiert USIZE
definiert als 8
hinein:
unsigned char *vsenc8(uint8_t *__restrict in, size_t n, unsigned char *__restrict out) ...
Die zweite rekursive Inklusion, mit USIZE
definiert als 16
erweitert die Vorlage wie folgt:
unsigned char *vsenc16(uint16_t *__restrict in, size_t n, unsigned char *__restrict out) ...
und 2 weitere Einschlüsse definieren vsenc32
und vsenc64
.
Diese Verwendung von vorverarbeitetem Quellcode ist bei separaten Dateien üblicher: eine für den instanziierenden Teil, der alle gemeinsamen Definitionen enthält, insbesondere die Makros, und eine separate Datei für die Code- und Datenvorlagen, die mehrmals mit verschiedenen Makrodefinitionen enthalten ist.
Ein gutes Beispiel ist die Generierung von Enums, Strings und Strukturen aus Arrays Atom und Operationscode Definitionen in QuickJS.
Die akzeptierte Antwort von @chqrlie erklärt zu 100%, was passiert. Dies ist nur ein ergänzender Kommentar.
Wenn Sie C++ verwenden, könnten wir zwei Vorlagenfunktionen definieren, um alle Implementierungen bereitzustellen vsenc8
, vsenc16
, vsenc32
, vsenc64
und vsdec8
, vsdec16
, vsdec32
, vsdec64
. Im Gegensatz dazu ist C jedoch eine sehr einfache Sprache und unterstützt keine Templates. Ein gängiger Trick, um die gleiche Leistung (in hässlicher Verpackung) zu haben, besteht darin, die dumme Makrofunktion der Sprache zu verwenden und den C-Präprozessor die entsprechende Arbeit für uns erledigen zu lassen. Die meisten C-Programmierer mit einiger Erfahrung werden im Laufe ihrer Karriere wiederholt auf diese Art von Konstrukt stoßen und sie verwenden.
Was dieses spezielle Beispiel etwas mühsam verständlich macht, ist, dass die Implementierungsdatei unkonventionell fünfmal geparst wird, um zuerst einige vorbereitende Definitionen und dann die vier Varianten der beiden Funktionen zu haben. Der erste Durchgang (innen #ifndef USIZE
Präprozessorblock) werden die benötigten Makros und Nicht-Varianten-Sachen definiert und rekursiv #include
sich viermal mit unterschiedlichen USIZE
Werte (8
, 16
, 32
, 64
) als Vorlagenwerte. Bei rekursiver Einbeziehung wird die entsprechende #else
Der Präprozessorblock wird mit dem Ergebnis von zwei Funktionen analysiert, die gemäß dem Wert von generiert werden USIZE
Makrokonstante, die für den Pass verwendet wird.
Ein konventionellerer, konzeptionell klarerer und sofort verständlicher Weg wäre, die Vorlagenfunktionen beispielsweise aus anderen Dateien einzubinden vsimple.impl
:
#define USIZE 8
/* Generate vsenc8(), vsdec8()... */
#include "vsimple.impl"
#undef USIZE
#define USIZE 16
/* Generate vsenc16(), vsdec16()... */
#include "vsimple.impl"
#undef USIZE
#define USIZE 32
/* Generate vsenc32(), vsdec32()... */
#include "vsimple.impl"
#undef USIZE
#define USIZE 64
/* Generate vsenc64(), vsdec64()... */
#include "vsimple.impl"
Die includierende Datei vsimple.c
und die enthaltene Datei vsimple.impl
könnten dann auch viel klarer organisiert werden, was sie wann definieren. Die meisten C-Programmierer würden das Implementierungsmuster erkennen und sofort wissen, was passiert.
Sich selbst auf diese Weise rekursiv und wiederholt einzubeziehen, erweckt ein Gefühl von Hokuspokus, das Applaus für einen verschleierten C-Wettbewerbsbeitrag hervorrufen würde, nicht jedoch für unternehmenskritischen Produktionscode.
Es ist Rekursion. Rekursion ist hier nützlich, da die C-Vorverarbeitung keine Schleifen hat. Darüber hinaus ist es wünschenswert, einen Trick mit einer Datei auszuführen, anstatt mehrere Dateien zu vermehren.
Angenommen, Sie müssten eine Funktion schreiben, die die Ganzzahlen von 1 bis 5 in eine Vorlagenzeichenfolge interpoliert und diese auf der Standardausgabe ausgibt. Angenommen, Sie müssten genau eine Funktion schreiben und durften keine Schleifen verwenden oder kopieren und einfügen printf
Aussagen. Sie könnten dies tun:
void template_print(const char *fmt, int n)
{
if (n == 0) {
template_print(fmt, 1);
template_print(fmt, 2);
template_print(fmt, 3);
template_print(fmt, 4);
template_print(fmt, 5);
} else {
/* imagine there are 30 lines of statements here we don't want
to repeat five times. */
printf(fmt, n);
}
}
Der oberste Aufruf dazu lautet dann template_print("whatever %d\n", 0)
unterscheidet sich durch das Nullargument der n
Parameter.
Der Aufruf der obersten Ebene mit 0 ist wie die anfängliche Verarbeitung von vsimple.c
ohne USIZE
definiert werden.
Die Anforderung an eine Funktion ist analog zu der Anforderung, eine einzelne, in sich geschlossene Funktion zu produzieren .c
Datei und nicht eine “Schnittstellen”-Datei, die #include
ist eine Implementierung.
Dadurch werden einige in Eclipse verwendete Indizierungssysteme beschädigt. Ich empfehle es nicht.
– Stark
13. Februar um 21:46 Uhr
Bitte geben Sie ein reproduzierbares Minimalbeispiel an. Es gibt Hinweise darauf, dass der betreffende Mechanismus nicht durch den gezeigten Code dargestellt wird. Das
#ifndef
s sind wichtig und wie USIZE verwendet wird, ist wichtig.– Yunnosch
13. Februar um 21:54 Uhr
@stark Andererseits würde ich sowieso nicht empfehlen, Eclipse für die C-Entwicklung zu verwenden. Und das zu unterbrechen, ist nur ein Grund mehr, es zu vermeiden.
– schwach
14. Februar um 7:49 Uhr
@Stark Wenn es gültiges C ist, muss Eclipse eindeutig repariert werden.
– Mark Morgan Lloyd
14. Februar um 18:28 Uhr
Eine Möglichkeit, um zu sehen, wie dies funktioniert, besteht darin, die Datei vorzuverarbeiten und sich die endgültige Ausgabe anzusehen. Wie
gcc -E foo.c
. Dadurch wird bestätigt, dass es nicht unendlich rekursiv ist, dh dass sich diese Includes innerhalb von befinden#ifdef
die du weggelassen hast. Der Inhalt der Ausgabe sollte Ihnen einen Hinweis darauf geben, was vor sich geht und worauf es ankommt.– Peter Cordes
14. Februar um 21:45 Uhr