Welche Anwendungen des ## Präprozessoroperators und der Fallstricke sind zu berücksichtigen?


87

Wie in vielen meiner vorherigen Fragen erwähnt, arbeite ich über K & R und bin derzeit im Präprozessor. Eines der interessanteren Dinge - etwas, das ich bei keinem meiner früheren Versuche, C zu lernen, zuvor gekannt habe - ist der ##Präprozessor-Operator. Laut K & R:

Der Präprozessoroperator ## bietet eine Möglichkeit, tatsächliche Argumente während der Makroerweiterung zu verketten. Wenn ein Parameter im Ersetzungstext neben a steht ##, wird der Parameter durch das eigentliche Argument ersetzt, der ##und der umgebende Leerraum werden entfernt und das Ergebnis wird erneut gescannt. Das Makro paste verkettet beispielsweise seine beiden Argumente:

#define paste(front, back) front ## back

So paste(name, 1)entsteht das Token name1.

Wie und warum sollte jemand dies in der realen Welt verwenden? Was sind praktische Beispiele für seine Verwendung und gibt es Fallstricke zu berücksichtigen?

Antworten:


47

CrashRpt: Verwenden von ## zum Konvertieren von Makro-Multibyte-Zeichenfolgen in Unicode

Eine interessante Verwendung in CrashRpt (Crash Reporting Library) ist die folgende:

#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
//Note you need a WIDEN2 so that __DATE__ will evaluate first.

Hier möchten sie eine Zwei-Byte-Zeichenfolge anstelle einer Ein-Byte-Zeichenfolge verwenden. Das sieht wahrscheinlich so aus, als wäre es wirklich sinnlos, aber sie tun es aus einem guten Grund.

 std::wstring BuildDate = std::wstring(WIDEN(__DATE__)) + L" " + WIDEN(__TIME__);

Sie verwenden es mit einem anderen Makro, das eine Zeichenfolge mit Datum und Uhrzeit zurückgibt.

Wenn Sie Lneben a setzen __ DATE __, erhalten Sie einen Kompilierungsfehler.


Windows: Verwenden von ## für generische Unicode- oder Multi-Byte-Zeichenfolgen

Windows verwendet etwa Folgendes:

#ifdef  _UNICODE
    #define _T(x)      L ## x
#else
    #define _T(x) x
#endif

Und _Twird überall im Code verwendet


Verschiedene Bibliotheken, die für saubere Accessor- und Modifikatornamen verwendet werden:

Ich habe auch gesehen, dass es im Code verwendet wird, um Accessoren und Modifikatoren zu definieren:

#define MYLIB_ACCESSOR(name) (Get##name)
#define MYLIB_MODIFIER(name) (Set##name)

Ebenso können Sie dieselbe Methode für alle anderen Arten der cleveren Namenserstellung verwenden.


Verschiedene Bibliotheken, mit denen mehrere Variablendeklarationen gleichzeitig erstellt werden:

#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;

3
Da Sie Zeichenfolgenliterale zur Kompilierungszeit verketten können, können Sie den BuildDate-Ausdruck auf std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__); die gesamte Zeichenfolge reduzieren und implizit auf einmal erstellen.
user666412

49

Wenn Sie die Vorverarbeitungsoperatoren Token-Paste (' ##') oder Stringizing (' #') verwenden, sollten Sie beachten, dass Sie eine zusätzliche Indirektionsebene verwenden müssen, damit sie in allen Fällen ordnungsgemäß funktionieren.

Wenn Sie dies nicht tun und die an den Token-Einfügeoperator übergebenen Elemente selbst Makros sind, erhalten Sie Ergebnisse, die wahrscheinlich nicht Ihren Wünschen entsprechen:

#include <stdio.h>

#define STRINGIFY2( x) #x
#define STRINGIFY(x) STRINGIFY2(x)
#define PASTE2( a, b) a##b
#define PASTE( a, b) PASTE2( a, b)

#define BAD_PASTE(x,y) x##y
#define BAD_STRINGIFY(x) #x

#define SOME_MACRO function_name

int main() 
{
    printf( "buggy results:\n");
    printf( "%s\n", STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( PASTE( SOME_MACRO, __LINE__)));

    printf( "\n" "desired result:\n");
    printf( "%s\n", STRINGIFY( PASTE( SOME_MACRO, __LINE__)));
}

Die Ausgabe:

buggy results:
SOME_MACRO__LINE__
BAD_PASTE( SOME_MACRO, __LINE__)
PASTE( SOME_MACRO, __LINE__)

desired result:
function_name21

1
Eine Erklärung dieses Präprozessorverhaltens finden Sie unter stackoverflow.com/questions/8231966/…
Adam Davis

@ MichaelBurr Ich habe deine Antwort gelesen und ich habe Zweifel. Wie kommt diese LINE druckt die Zeilennummer?
HILFE PLZ

3
@AbhimanyuAryan: Ich bin nicht sicher, ob Sie danach fragen, aber es __LINE__handelt sich um einen speziellen Makronamen, der durch den Präprozessor durch die aktuelle Zeilennummer der Quelldatei ersetzt wird.
Michael Burr

Es wäre cool, wenn Sprachspezifikationen zitiert / verlinkt werden könnten, wie hier
Antonio

14

Hier ist ein Fall, auf den ich beim Upgrade auf eine neue Version eines Compilers gestoßen bin:

Die unnötige Verwendung des Token-Pasting-Operators ( ##) ist nicht portierbar und kann zu unerwünschten Leerzeichen, Warnungen oder Fehlern führen.

Wenn das Ergebnis des Token-Einfügeoperators kein gültiges Präprozessor-Token ist, ist der Token-Einfügeoperator unnötig und möglicherweise schädlich.

Beispielsweise könnte man versuchen, String-Literale zur Kompilierungszeit mit dem Operator zum Einfügen von Token zu erstellen:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a##+##b)
#define NS(a, b) STRINGIFY(a##::##b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

Bei einigen Compilern wird das erwartete Ergebnis ausgegeben:

1+2 std::vector

Bei anderen Compilern umfasst dies unerwünschte Leerzeichen:

1 + 2 std :: vector

Ziemlich moderne Versionen von GCC (> = 3.3 oder so) können diesen Code nicht kompilieren:

foo.cpp:16:1: pasting "1" and "+" does not give a valid preprocessing token
foo.cpp:16:1: pasting "+" and "2" does not give a valid preprocessing token
foo.cpp:16:1: pasting "std" and "::" does not give a valid preprocessing token
foo.cpp:16:1: pasting "::" and "vector" does not give a valid preprocessing token

Die Lösung besteht darin, den Operator zum Einfügen von Token wegzulassen, wenn Präprozessor-Token mit C / C ++ - Operatoren verkettet werden:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a+b)
#define NS(a, b) STRINGIFY(a::b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

Das Kapitel zur GCC CPP-Dokumentation zur Verkettung enthält weitere nützliche Informationen zum Operator zum Einfügen von Token.


Danke - das war mir nicht bewusst (aber dann benutze ich diese Vorverarbeitungsoperatoren nicht zu oft ...).
Michael Burr

3
Es wird aus einem bestimmten Grund als "Token-Einfügen" -Operator bezeichnet. Die Absicht ist, am Ende ein einzelnes Token zu erhalten, wenn Sie fertig sind. Schöne Zusammenfassung.
Mark Ransom

Wenn das Ergebnis des Token-Einfügeoperators kein gültiges Präprozessor-Token ist, ist das Verhalten undefiniert.
Alecov

Sprachänderungen wie hexadezimale Gleitkommazahlen oder (in C ++) Zifferntrennzeichen und benutzerdefinierte Literale ändern ständig das, was ein "gültiges Vorverarbeitungstoken" darstellt. Bitte missbrauchen Sie es niemals so! Wenn Sie (sprachliche) Token trennen müssen, buchstabieren Sie sie bitte als zwei separate Token und verlassen Sie sich nicht auf zufällige Interaktionen zwischen der Präprozessor-Grammatik und der richtigen Sprache.
Kerrek SB

6

Dies ist in allen möglichen Situationen nützlich, um sich nicht unnötig zu wiederholen. Das Folgende ist ein Beispiel aus dem Emacs-Quellcode. Wir möchten eine Reihe von Funktionen aus einer Bibliothek laden. Die Funktion "foo" sollte zugewiesen fn_foowerden und so weiter. Wir definieren das folgende Makro:

#define LOAD_IMGLIB_FN(lib,func) {                                      \
    fn_##func = (void *) GetProcAddress (lib, #func);                   \
    if (!fn_##func) return 0;                                           \
  }

Wir können es dann verwenden:

LOAD_IMGLIB_FN (library, XpmFreeAttributes);
LOAD_IMGLIB_FN (library, XpmCreateImageFromBuffer);
LOAD_IMGLIB_FN (library, XpmReadFileToImage);
LOAD_IMGLIB_FN (library, XImageFree);

Der Vorteil ist, dass Sie nicht beide schreiben müssen fn_XpmFreeAttributesund "XpmFreeAttributes"(und das Risiko eingehen, einen von ihnen falsch zu schreiben ).


4

In einer früheren Frage zum Stapelüberlauf wurde nach einer reibungslosen Methode zum Generieren von Zeichenfolgendarstellungen für Aufzählungskonstanten ohne viel fehleranfälliges erneutes Eingeben gefragt.

Verknüpfung

Meine Antwort auf diese Frage zeigte, wie Sie mit wenig Präprozessormagie Ihre Aufzählung wie folgt definieren können (zum Beispiel) ...;

ENUM_BEGIN( Color )
  ENUM(RED),
  ENUM(GREEN),
  ENUM(BLUE)
ENUM_END( Color )

... mit dem Vorteil, dass die Makroerweiterung nicht nur die Aufzählung (in einer .h-Datei) definiert, sondern auch ein passendes Array von Zeichenfolgen (in einer .c-Datei);

const char *ColorStringTable[] =
{
  "RED",
  "GREEN",
  "BLUE"
};

Der Name der Zeichenfolgentabelle ergibt sich aus dem Einfügen des Makroparameters (dh Farbe) in StringTable mit dem Operator ##. Bei solchen Anwendungen (Tricks?) Sind die Operatoren # und ## von unschätzbarem Wert.


3

Sie können das Einfügen von Token verwenden, wenn Sie Makroparameter mit etwas anderem verketten müssen.

Es kann für Vorlagen verwendet werden:

#define LINKED_LIST(A) struct list##_##A {\
A value; \
struct list##_##A *next; \
};

In diesem Fall würde LINKED_LIST (int) Ihnen geben

struct list_int {
int value;
struct list_int *next;
};

Ebenso können Sie eine Funktionsvorlage für die Listenüberquerung schreiben.


2

Ich verwende es in C-Programmen, um die Prototypen für eine Reihe von Methoden korrekt durchzusetzen, die einer Art Aufrufkonvention entsprechen müssen. In gewisser Weise kann dies für die Objektorientierung des armen Mannes in geradem C verwendet werden:

SCREEN_HANDLER( activeCall )

erweitert sich zu so etwas:

STATUS activeCall_constructor( HANDLE *pInst )
STATUS activeCall_eventHandler( HANDLE *pInst, TOKEN *pEvent );
STATUS activeCall_destructor( HANDLE *pInst );

Dies erzwingt die korrekte Parametrisierung für alle "abgeleiteten" Objekte, wenn Sie Folgendes tun:

SCREEN_HANDLER( activeCall )
SCREEN_HANDLER( ringingCall )
SCREEN_HANDLER( heldCall )

Das Obige in Ihren Header-Dateien usw. Es ist auch nützlich für die Wartung, wenn Sie die Definitionen ändern und / oder den "Objekten" Methoden hinzufügen möchten.


2

SGlib verwendet ##, um Vorlagen in C grundsätzlich zu fudgen . Da keine Funktionsüberladung vorliegt, wird ## verwendet, um den Typnamen in die Namen der generierten Funktionen zu kleben. Wenn ich einen Listentyp namens list_t hätte, würde ich Funktionen wie sglib_list_t_concat usw. erhalten.


2

Ich verwende es für einen Home Rolled Assert auf einem nicht standardmäßigen C-Compiler für Embedded:



#define ASSERT(exp) if(!(exp)){ \
                      print_to_rs232("Assert failed: " ## #exp );\
                      while(1){} //Let the watchdog kill us 



3
Ich nehme an, Sie meinen mit "Nicht-Standard", dass der Compiler kein String-Einfügen, sondern Token-Einfügen durchgeführt hat - oder hätte es auch ohne funktioniert ##?
PJTraill

1

Ich verwende es zum Hinzufügen von benutzerdefinierten Präfixen zu Variablen, die durch Makros definiert sind. Also so etwas wie:

UNITTEST(test_name)

erweitert sich zu:

void __testframework_test_name ()

1

Die Hauptanwendung ist, wenn Sie eine Namenskonvention haben und Ihr Makro diese Namenskonvention nutzen soll. Möglicherweise haben Sie mehrere Methodenfamilien: image_create (), image_activate () und image_release () sowie file_create (), file_activate (), file_release () und mobile_create (), mobile_activate () und mobile_release ().

Sie könnten ein Makro für die Behandlung des Objektlebenszyklus schreiben:

#define LIFECYCLE(name, func) (struct name x = name##_create(); name##_activate(x); func(x); name##_release())

Natürlich ist eine Art "minimale Version von Objekten" nicht die einzige Art von Namenskonvention, für die dies gilt - fast die überwiegende Mehrheit der Namenskonventionen verwendet eine gemeinsame Teilzeichenfolge, um die Namen zu bilden. Es könnte mir Funktionsnamen (wie oben) oder Feldnamen, Variablennamen oder fast alles andere geben.


1

Eine wichtige Verwendung in WinCE:

#define BITFMASK(bit_position) (((1U << (bit_position ## _WIDTH)) - 1) << (bit_position ## _LEFTSHIFT))

Bei der Definition der Registerbitbeschreibung gehen wir wie folgt vor:

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

Und während Sie BITFMASK verwenden, verwenden Sie einfach:

BITFMASK(ADDR)

0

Es ist sehr nützlich für die Protokollierung. Du kannst tun:

#define LOG(msg) log_msg(__function__, ## msg)

Oder, wenn Ihr Compiler unterstützt keine Funktion und func :

#define LOG(msg) log_msg(__file__, __line__, ## msg)

Die obigen "Funktionen" protokollieren die Nachricht und zeigen genau, welche Funktion eine Nachricht protokolliert hat.

Meine C ++ - Syntax ist möglicherweise nicht ganz korrekt.


1
Was hast du damit versucht? Ohne "##" würde es genauso gut funktionieren, da kein Token-Einfügen "," bis "msg" erforderlich ist. Haben Sie versucht, msg zu stringifizieren? Außerdem müssen FILE und LINE in Großbuchstaben und nicht in Kleinbuchstaben geschrieben sein.
bk1e

Du hast in der Tat recht. Ich muss das ursprüngliche Skript finden, um zu sehen, wie ## verwendet wurde. Schande über mich, heute kein Keks!
ya23
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.