Jakie są zastosowania operatora preprocesora ## i jakie rozwiązania należy wziąć pod uwagę?

88

Jak wspomniano w wielu moich poprzednich pytaniach, pracuję przez K&R i obecnie używam preprocesora. Jedną z bardziej interesujących rzeczy - czymś, czego nigdy wcześniej nie wiedziałem z żadnej z moich poprzednich prób nauki C - jest ##operator preprocesora. Według K&R:

Operator preprocesora ## umożliwia konkatenację rzeczywistych argumentów podczas rozwijania makr. Jeśli parametr w zastępczym tekście sąsiaduje z a ##, parametr jest zastępowany przez rzeczywisty argument, ##biały znak i otaczający go odstęp są usuwane, a wynik jest ponownie skanowany. Na przykład makropaste łączy dwa argumenty:

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

więc paste(name, 1)tworzy token name1.

Jak i dlaczego ktoś miałby to wykorzystać w prawdziwym świecie? Jakie są praktyczne przykłady jego użycia i czy należy wziąć pod uwagę pewne kwestie?

John Rudy
źródło

Odpowiedzi:

47

CrashRpt: Używanie ## do konwersji wielobajtowych ciągów makr na Unicode

Ciekawym zastosowaniem w CrashRpt (bibliotece raportowania awarii) jest:

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

Tutaj chcą użyć ciągu dwubajtowego zamiast ciągu jednobajtowego na znak. Prawdopodobnie wygląda to na bezcelowe, ale robią to nie bez powodu.

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

Używają go z innym makrem, które zwraca ciąg z datą i godziną.

Umieszczenie Lobok a __ DATE __spowodowałoby błąd kompilacji.


Windows: Używanie ## dla ogólnych ciągów Unicode lub wielobajtowych

Windows używa czegoś podobnego do następującego:

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

I _Tjest używany wszędzie w kodzie


Różne biblioteki, używane do czyszczenia nazw akcesorów i modyfikatorów:

Widziałem również, że jest używany w kodzie do definiowania akcesorów i modyfikatorów:

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

Podobnie możesz użyć tej samej metody do każdego innego rodzaju sprytnego tworzenia nazw.


Różne biblioteki, używające go do tworzenia kilku deklaracji zmiennych naraz:

#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;
Brian R. Bondy
źródło
3
Ponieważ można łączyć literały ciągów w czasie kompilacji, można zredukować wyrażenie BuildDate do std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__); i niejawnie skompilować cały ciąg naraz.
user666412
49

Jedna rzecz, o której należy pamiętać, gdy używasz token-paste („ ##”) lub stringizing („# operatorów przetwarzania wstępnego '), jest to, że musisz użyć dodatkowego poziomu pośredniego, aby działały poprawnie we wszystkich przypadkach.

Jeśli tego nie zrobisz, a elementy przekazane do operatora wklejania tokenów są same w sobie makrami, otrzymasz wyniki, które prawdopodobnie nie są tym, czego oczekujesz:

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

Wyjście:

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

desired result:
function_name21
Michael Burr
źródło
1
Aby uzyskać wyjaśnienie tego zachowania preprocesora, zobacz stackoverflow.com/questions/8231966/ ...
Adam Davis,
@MichaelBurr Czytałem Twoją odpowiedź i mam wątpliwości. Jak to się stało, że ta LINIA drukuje numer linii?
HELP PLZ
3
@AbhimanyuAryan: Nie jestem pewien, czy o to pytasz, ale __LINE__jest to specjalna nazwa makra, która jest zastępowana przez preprocesor z bieżącym numerem wiersza pliku źródłowego.
Michael Burr
Byłoby fajnie, gdyby specyfikacje językowe można było cytować / linkować, jak tutaj
Antonio
14

Oto pułapka, na którą natknąłem się podczas aktualizacji do nowej wersji kompilatora:

Niepotrzebne użycie operatora wklejania tokenów ( ##) jest nieprzenośne i może generować niepożądane spacje, ostrzeżenia lub błędy.

Gdy wynik operatora wklejania tokenu nie jest prawidłowym tokenem preprocesora, operator wklejania tokenu jest niepotrzebny i prawdopodobnie szkodliwy.

Na przykład można spróbować zbudować literały ciągów w czasie kompilacji przy użyciu operatora token-pasting:

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

W niektórych kompilatorach zwróci to oczekiwany wynik:

1+2 std::vector

Na innych kompilatorach będzie to zawierać niepożądane spacje:

1 + 2 std :: vector

Dość nowoczesne wersje GCC (> = 3.3 lub więcej) nie będą w stanie skompilować tego kodu:

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

Rozwiązaniem jest pominięcie operatora wklejania tokenów podczas łączenia tokenów preprocesora z operatorami C / C ++:

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

GCC CPP dokumentacji rozdział na łączenie jest bardziej użyteczne informacje dotyczące operatora tokenu wklejenie.

bk1e
źródło
Dzięki - nie byłem tego świadomy (ale wtedy nie używam zbyt często tych operatorów przetwarzania wstępnego ...).
Michael Burr
3
Nie bez powodu nazywa się to operatorem „wklejania tokenów” - celem jest uzyskanie pojedynczego tokena, gdy skończysz. Niezły opis.
Mark Ransom
Gdy wynik operatora wklejania tokenu nie jest prawidłowym tokenem preprocesora, zachowanie jest niezdefiniowane.
alecov
Zmiany języka, takie jak szesnastkowe liczby zmiennoprzecinkowe lub (w C ++) separatory cyfr i literały zdefiniowane przez użytkownika, nieustannie zmieniają to, co stanowi „prawidłowy token przetwarzania wstępnego”, więc nigdy nie nadużywaj go w ten sposób! Jeśli musisz rozdzielić (właściwe językowo) tokeny, przeliteruj je jako dwa osobne tokeny i nie polegaj na przypadkowych interakcjach między gramatyką preprocesora a właściwym językiem.
Kerrek SB
6

Jest to przydatne we wszystkich sytuacjach, aby nie powtarzać się niepotrzebnie. Poniżej znajduje się przykład z kodu źródłowego Emacsa. Chcielibyśmy załadować szereg funkcji z biblioteki. Należy przypisać funkcję „foo” fn_fooi tak dalej. Definiujemy następujące makro:

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

Możemy go wtedy użyć:

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

Korzyścią jest brak konieczności zapisywania obu fn_XpmFreeAttributesi "XpmFreeAttributes"(i ryzyko błędnej pisowni jednego z nich).

Vebjorn Ljosa
źródło
4

Poprzednie pytanie dotyczące przepełnienia stosu dotyczyło płynnej metody generowania reprezentacji ciągów dla stałych wyliczenia bez częstego ponownego wpisywania, które może powodować błędy.

Połączyć

Moja odpowiedź na to pytanie pokazała, jak zastosowanie małej magii preprocesora pozwala zdefiniować wyliczenie w ten sposób (na przykład) ...;

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

... Z tą korzyścią, że rozwinięcie makr nie tylko definiuje wyliczenie (w pliku .h), ale także definiuje pasującą tablicę ciągów (w pliku .c);

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

Nazwa tabeli ciągów pochodzi z wklejenia parametru makra (tj. Color) do StringTable przy użyciu operatora ##. Takie aplikacje (sztuczki?) To te, w których operatory # i ## są nieocenione.

Bill Forster
źródło
3

Możesz użyć wklejania tokenów, gdy chcesz połączyć parametry makra z czymś innym.

Może być używany do szablonów:

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

W tym przypadku LINKED_LIST (int) dałoby ci

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

Podobnie możesz napisać szablon funkcji do przeglądania listy.

qrdl
źródło
2

Używam go w programach C, aby pomóc poprawnie wymusić prototypy zestawu metod, które muszą być zgodne z jakąś konwencją wywoływania. W pewnym sensie można to wykorzystać do orientacji obiektu dla biednego człowieka w prostym C:

SCREEN_HANDLER( activeCall )

rozwija się do czegoś takiego:

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

Wymusza to poprawną parametryzację dla wszystkich obiektów „pochodnych”, gdy:

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

powyższe w plikach nagłówkowych, itp. Jest to również przydatne przy konserwacji, jeśli zdarzy ci się nawet zmienić definicje i / lub dodać metody do "obiektów".

Wysoki Jeff
źródło
2

SGlib używa ## do prostowania szablonów w C. Ponieważ nie ma przeciążenia funkcji, ## jest używany do wklejania nazwy typu do nazw generowanych funkcji. Gdybym miał typ listy o nazwie list_t, otrzymywałbym funkcje o nazwie takiej jak sglib_list_t_concat i tak dalej.


źródło
2

Używam go do asercji domowej na niestandardowym kompilatorze C dla osadzonych:



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


c0m4
źródło
3
Rozumiem, że przez „niestandardowy” masz na myśli to, że kompilator nie wklejał ciągów, ale wklejał token - czy też działałby nawet bez ##?
PJTraill
1

Używam go do dodawania niestandardowych przedrostków do zmiennych zdefiniowanych przez makra. Więc coś takiego:

UNITTEST(test_name)

rozwija się do:

void __testframework_test_name ()
John Millikin
źródło
1

Głównym zastosowaniem jest sytuacja, gdy masz konwencję nazewnictwa i chcesz, aby makro wykorzystywało tę konwencję nazewnictwa. Być może masz kilka rodzin metod: image_create (), image_activate () i image_release () także file_create (), file_activate (), file_release () i mobile_create (), mobile_activate () i mobile_release ().

Możesz napisać makro do obsługi cyklu życia obiektu:

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

Oczywiście coś w rodzaju „minimalnej wersji obiektów” nie jest jedynym rodzajem konwencji nazewnictwa, do którego się to odnosi - prawie większość konwencji nazewnictwa używa wspólnego podłańcucha do tworzenia nazw. Mógłby to być nazwy funkcji (jak powyżej) lub nazwy pól, nazwy zmiennych lub prawie wszystko inne.

mcherm
źródło
1

Jedno ważne zastosowanie w WinCE:

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

Definiując opis bitów rejestru wykonujemy następujące czynności:

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

A korzystając z BITFMASK, po prostu użyj:

BITFMASK(ADDR)
Keshava GN
źródło
0

Jest to bardzo przydatne do logowania. Możesz to zrobić:

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

Lub, jeśli Twój kompilator nie obsługuje funkcji i func :

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

Powyższe „funkcje” rejestrują komunikat i pokazują dokładnie, która funkcja zarejestrowała komunikat.

Moja składnia C ++ może nie być całkiem poprawna.

ya23
źródło
1
Co próbowałeś z tym zrobić? To działałoby równie dobrze bez znaku „##”, ponieważ nie ma potrzeby wklejania tokenów do „msg”. Czy próbowałeś uszeregować wiadomość? Ponadto FILE i LINE muszą być zapisane wielkimi literami, a nie małymi literami.
bk1e,
Rzeczywiście masz rację. Muszę znaleźć oryginalny skrypt, aby zobaczyć, jak został użyty ##. Wstyd mi, dziś żadnych ciasteczek!
ya23