Po co używać pozornie bezsensownych instrukcji do-while i if-else w makrach?


787

W wielu makrach C / C ++ widzę kod makra zawinięty w coś, co wydaje się pozbawioną znaczenia do whilepętlą. Oto przykłady.

#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else

Nie widzę co do whilerobi. Dlaczego po prostu tego nie napisać?

#define FOO(X) f(X); g(X)

2
Dla przykładu z innym, dodałbym wyrażenie voidtypu na końcu ... jak ((void) 0) .
Phil1970

1
Przypomnienie, że do whilekonstrukcja nie jest zgodna z instrukcjami return, więc if (1) { ... } else ((void)0)konstrukcja ma więcej zgodnych zastosowań w standardzie C. A w GNU C wolisz konstrukcję opisaną w mojej odpowiedzi.
Cœur,

Odpowiedzi:


829

do ... whilei if ... elsesą po to, aby średnik po makrze zawsze oznaczał to samo. Powiedzmy, że masz coś jak drugie makro.

#define BAR(X) f(x); g(x)

Teraz, jeśli miałbyś użyć BAR(X);w if ... elsewyciągu, w którym ciała wyciągu if nie były zawinięte w nawiasy klamrowe, dostałbyś złą niespodziankę.

if (corge)
  BAR(corge);
else
  gralt();

Powyższy kod rozwinąłby się w

if (corge)
  f(corge); g(corge);
else
  gralt();

który jest niepoprawny pod względem składniowym, ponieważ reszta nie jest już powiązana z if. Nie pomaga zawijać makr w nawiasy klamrowe, ponieważ średnik po nawiasach jest niepoprawny pod względem składniowym.

if (corge)
  {f(corge); g(corge);};
else
  gralt();

Istnieją dwa sposoby rozwiązania problemu. Pierwszym jest użycie przecinka do sekwencjonowania instrukcji w makrze bez pozbawiania go zdolności do działania jak wyrażenie.

#define BAR(X) f(X), g(X)

Powyższa wersja paska BARrozszerza powyższy kod w następujący sposób, który jest poprawny pod względem składniowym.

if (corge)
  f(corge), g(corge);
else
  gralt();

To nie działa, jeśli zamiast tego f(X)masz bardziej skomplikowany kod, który musi przejść we własnym bloku, na przykład, aby zadeklarować zmienne lokalne. W najbardziej ogólnym przypadku rozwiązaniem jest użycie czegoś takiego, do ... whileaby makro stało się pojedynczą instrukcją, która przyjmuje średnik bez zamieszania.

#define BAR(X) do { \
  int i = f(X); \
  if (i > 4) g(i); \
} while (0)

Nie musisz go używać do ... while, możesz też coś ugotować if ... else, chociaż kiedy if ... elserozszerza się wewnątrz if ... else, prowadzi to do „ wiszącego innego ”, co może sprawić, że istniejący problem z wiszącym innym jeszcze trudniej będzie znaleźć, jak w poniższym kodzie .

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

Chodzi o to, aby użyć średnika w kontekstach, w których wiszący średnik jest błędny. Oczywiście w tym miejscu można (i prawdopodobnie należy) argumentować, że lepiej byłoby zadeklarować BARjako rzeczywistą funkcję, a nie makro.

Podsumowując, do ... whilejest tam, aby obejść niedociągnięcia preprocesora C. Kiedy te przewodniki w stylu C każą ci zrezygnować z preprocesora C, martwią się o to.


18
Czy nie jest to mocny argument, aby zawsze używać nawiasów klamrowych w instrukcjach if, while i for? Jeśli zawsze starasz się to robić (jak jest to na przykład wymagane w przypadku MISRA-C), problem opisany powyżej zniknie.
Steve Melnikoff

17
Przykładem przecinka powinien być #define BAR(X) (f(X), g(X))priorytet operatora, który może zepsuć semantykę.
Stewart

20
@DawidFerenczy: chociaż zarówno ty, jak i ja, sprzed czterech i pół lat temu, masz rację, musimy żyć w prawdziwym świecie. O ile nie możemy zagwarantować, że wszystkie ifinstrukcje itp. W naszym kodzie używają nawiasów klamrowych, wówczas owijanie makr w ten sposób jest prostym sposobem uniknięcia problemów.
Steve Melnikoff,

8
Uwaga: if(1) {...} else void(0)formularz jest bezpieczniejszy niż do {...} while(0)dla makr, których parametrami jest kod zawarty w rozszerzeniu makra, ponieważ nie zmienia zachowania słów kluczowych break ani kontynuowania. Na przykład: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }powoduje nieoczekiwane zachowanie, gdy MYMACROjest zdefiniowane jako #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)ponieważ przerwa wpływa na pętlę while makra, a nie na pętlę for w miejscu wywołania makra.
Chris Kline,

5
To znaczy, @ace void(0)było literówką (void)0. I wierzę, że to nie rozwiąże „wiszących” inny problem: zawiadomienie nie ma średnik po (void)0. W przeciwnym razie wiszące (np. if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) Powoduje błąd kompilacji. Oto żywo przykładem wykazania go
Chris Kline

153

Makra to kopiowane / wklejane fragmenty tekstu, które procesor umieści w oryginalnym kodzie; autor makra ma nadzieję, że zamiana wygeneruje prawidłowy kod.

Istnieją trzy dobre „wskazówki”, aby odnieść sukces:

Pomóż makrze zachowywać się jak prawdziwy kod

Normalny kod jest zwykle zakończony średnikiem. Czy użytkownik powinien wyświetlić kod, który go nie potrzebuje ...

doSomething(1) ;
DO_SOMETHING_ELSE(2)  // <== Hey? What's this?
doSomethingElseAgain(3) ;

Oznacza to, że użytkownik oczekuje, że kompilator wygeneruje błąd, jeśli średnik jest nieobecny.

Ale naprawdę dobrym powodem jest to, że w pewnym momencie autor makra będzie musiał zastąpić makro prawdziwą funkcją (być może wbudowaną). Tak więc makro powinno naprawdę zachowywać się jak jedno.

Powinniśmy więc mieć makro wymagające średnika.

Utwórz poprawny kod

Jak pokazano w odpowiedzi jfm3, czasami makro zawiera więcej niż jedną instrukcję. A jeśli makro zostanie użyte w instrukcji if, będzie to problematyczne:

if(bIsOk)
   MY_MACRO(42) ;

To makro można rozwinąć jako:

#define MY_MACRO(x) f(x) ; g(x)

if(bIsOk)
   f(42) ; g(42) ; // was MY_MACRO(42) ;

gFunkcja zostanie wykonana niezależnie od wartości bIsOk.

Oznacza to, że musimy dodać zakres do makra:

#define MY_MACRO(x) { f(x) ; g(x) ; }

if(bIsOk)
   { f(42) ; g(42) ; } ; // was MY_MACRO(42) ;

Utwórz poprawny kod 2

Jeśli makro przypomina:

#define MY_MACRO(x) int i = x + 1 ; f(i) ;

Możemy mieć inny problem w następującym kodzie:

void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

Ponieważ rozwinąłby się jako:

void doSomething()
{
    int i = 25 ;
    int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}

Oczywiście ten kod się nie skompiluje. Zatem ponownie rozwiązanie wykorzystuje zakres:

#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }

void doSomething()
{
    int i = 25 ;
    { int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}

Kod znów działa poprawnie.

Łącząc efekty średnika + zakresu?

Jest jeden idiom C / C ++, który wywołuje ten efekt: Pętla do / while:

do
{
    // code
}
while(false) ;

Do / while może utworzyć zakres, w ten sposób enkapsulując kod makra, i na końcu potrzebuje średnika, przekształcając się w kod, który go potrzebuje.

Bonus?

Kompilator C ++ zoptymalizuje pętlę do / while, ponieważ fakt, że jego stan końcowy jest fałszywy, jest znany w czasie kompilacji. Oznacza to, że makro takie jak:

#define MY_MACRO(x)                                  \
do                                                   \
{                                                    \
    const int i = x + 1 ;                            \
    f(i) ; g(i) ;                                    \
}                                                    \
while(false)

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      MY_MACRO(42) ;

   // Etc.
}

rozwinie się poprawnie jako

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      do
      {
         const int i = 42 + 1 ; // was MY_MACRO(42) ;
         f(i) ; g(i) ;
      }
      while(false) ;

   // Etc.
}

a następnie jest kompilowany i optymalizowany jako

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
   {
      f(43) ; g(43) ;
   }

   // Etc.
}

6
Zauważ, że zmiana makr na funkcję wbudowaną zmienia niektóre standardowe predefiniowane makra, np. Poniższy kod pokazuje zmianę FUNKCJI i LINII : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNKCJA , LINE ) inline void Finline () {printf ("% s% d \ n", FUNKCJA , LINIA ); } int main () {Fmacro (); Finline (); zwraca 0; } (pogrubione terminy powinny być ujęte w podwójne podkreślenia - zły
formatator

6
Istnieje wiele drobnych, ale nie całkowicie nieistotnych problemów z tą odpowiedzią. Na przykład: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }nie jest poprawnym rozszerzeniem; xw ekspansji powinno być 32. Bardziej złożony problem jest to, co jest ekspansja MY_MACRO(i+7). Kolejnym jest rozszerzenie MY_MACRO(0x07 << 6). Jest wiele dobrych rzeczy, ale jest kilka nieoznaczonych liter „i” i liter „nie”.
Jonathan Leffler

@Gnubie: Wydaje mi się, że wciąż tu jesteś i do tej pory tego nie rozgryzłeś: możesz uciec gwiazdkom i podkreśleniom w komentarzach za pomocą ukośników odwrotnych, więc jeśli wpiszesz \_\_LINE\_\_, renderuje się jako __LINE__. IMHO, lepiej po prostu użyć formatowania kodu dla kodu; na przykład __LINE__(co nie wymaga specjalnej obsługi). PS Nie wiem, czy to była prawda w 2012 roku; od tego czasu wprowadzili sporo ulepszeń do silnika.
Scott,

1
Doceniając fakt, że mój komentarz jest spóźniony o sześć lat, ale większość kompilatorów C w rzeczywistości nie obsługuje inlinefunkcji wbudowanych (zgodnie ze standardem)
Andrew

53

@ jfm3 - Masz ładną odpowiedź na pytanie. Możesz także dodać, że makro idiom zapobiega również potencjalnie bardziej niebezpiecznym (ponieważ nie ma błędu) niezamierzonemu zachowaniu za pomocą prostych instrukcji „jeśli”:

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

rozwija się do:

if (test) f(baz); g(baz);

co jest poprawne pod względem składniowym, więc nie występuje błąd kompilatora, ale ma prawdopodobnie niezamierzoną konsekwencję, że zawsze będzie wywoływana g ().


22
„prawdopodobnie niezamierzony”? Powiedziałbym, że „na pewno niezamierzony”, w przeciwnym razie programista musi zostać wyjęty i zastrzelony (w przeciwieństwie do okrągłego karania batem).
Lawrence Dol

4
Lub mogą dostać podwyżkę, jeśli pracują dla trzyliterowej agencji i potajemnie wstawiają ten kod do powszechnie używanego programu open source ... :-)
R .. GitHub ZATRZYMAJ POMOC ICE

2
I ten komentarz przypomina mi tylko linię błędu goto w ostatnim błędzie weryfikacji certyfikatu SSL znalezionym w systemach Apple
Gerard Sexton

23

Powyższe odpowiedzi wyjaśniają znaczenie tych konstruktów, ale istnieje znacząca różnica między tymi dwoma, o których nie wspomniano. W rzeczywistości nie jest to powód do preferują do ... whiledo if ... elsekonstruktu.

Problemem if ... elsekonstrukcji jest to, że nie zmusza cię do wstawienia średnika. Jak w tym kodzie:

FOO(1)
printf("abc");

Chociaż pominęliśmy średnik (przez pomyłkę), kod zostanie rozwinięty do

if (1) { f(X); g(X); } else
printf("abc");

i po cichu się skompiluje (chociaż niektóre kompilatory mogą wyświetlać ostrzeżenie o niedostępnym kodzie). Ale printfoświadczenie nigdy nie zostanie wykonane.

do ... whilekonstrukcja nie ma takiego problemu, ponieważ jedynym prawidłowym tokenem po średniku while(0)jest średnik.


2
@RichardHansen: Wciąż nie jest tak dobry, ponieważ patrząc na wywołanie makra nie wiesz, czy rozwija się ono do instrukcji, czy do wyrażenia. Jeśli ktoś przyjmie to później, może napisać, FOO(1),x++;co ponownie da nam fałszywy pozytyw. Po prostu użyj do ... whilei to wszystko.
Jakow Galka

1
Wystarczy udokumentować makro, aby uniknąć nieporozumień. Zgadzam się, że do ... while (0)jest to preferowane, ale ma jedną wadę: A breaklub continuebędzie kontrolować do ... while (0)pętlę, a nie pętlę zawierającą wywołanie makra. Więc ifsztuczka wciąż ma wartość.
Richard Hansen

2
Nie widzę, gdzie można umieścić a breaklub a, continuektóry byłby widoczny jako wewnątrz do {...} while(0)pseudo-pętli makr . Nawet w parametrze makra spowodowałoby to błąd składniowy.
Patrick Schlüter,

5
Innym powodem użycia do { ... } while(0)zamiast if whateverkonstrukcji jest jego idiomatyczna natura. do {...} while(0)Konstrukt jest powszechny, dobrze znany i używany przez wiele wielu programistów. Jego uzasadnienie i dokumentacja są łatwo znane. Nie w przypadku ifkonstruktu. Dlatego przeglądanie kodu zajmuje mniej wysiłku.
Patrick Schlüter,

1
@tristopia: Widziałem, jak ludzie piszą makra, które biorą bloki kodu jako argumenty (co niekoniecznie polecam). Na przykład: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Może być użyty jak CHECK(system("foo"), break;);, gdzie break;ma odnosić się do pętli otaczającej CHECK()wywołanie.
Richard Hansen

16

Oczekuje się, że kompilatory optymalizują do { ... } while(false);pętle, ale istnieje inne rozwiązanie, które nie wymagałoby takiej konstrukcji. Rozwiązaniem jest użycie przecinka:

#define FOO(X) (f(X),g(X))

lub jeszcze bardziej egzotycznie:

#define FOO(X) g((f(X),(X)))

Chociaż będzie to działać dobrze z osobnymi instrukcjami, nie będzie działać z przypadkami, w których zmienne są konstruowane i używane jako część #define:

#define FOO(X) (int s=5,f((X)+s),g((X)+s))

W tym przypadku konieczne byłoby użycie konstrukcji do / while.


dzięki, ponieważ operator przecinka nie gwarantuje kolejności wykonywania, to zagnieżdżanie jest sposobem na wymuszenie tego.
Marius

15
@Marius: False; operator przecinek punkt sekwencji i tym samym ma gwarancji, kolejność wykonywania. Podejrzewam, że pomyliłeś to z przecinkami na listach argumentów funkcji.
R .. GitHub ZATRZYMAJ LÓD

2
Druga egzotyczna sugestia przyniosła mi dzień.
Spidey,

Chciałem tylko dodać, że kompilatory są zmuszone do zachowania obserwowalnego programu, więc optymalizacja do / away away nie jest wielkim problemem (zakładając, że optymalizacje kompilatora są prawidłowe).
Marco A.

@MarcoA. podczas gdy masz rację, w przeszłości odkryłem, że optymalizacja kompilatora, przy jednoczesnym zachowaniu funkcji kodu, ale zmieniając linie, które wydają się nic nie robić w pojedynczym kontekście, złamie wielowątkowe algorytmy. W tym przypadku Peterson's Algorithm.
Marius,

11

Biblioteka preprocesora Jensa Gustedta P99 (tak, fakt, że coś takiego istnieje, zaskoczyła mnie też!) Poprawia if(1) { ... } elsekonstrukcję w niewielki, ale znaczący sposób, definiując:

#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP

Uzasadnieniem tego jest to, że w przeciwieństwie do do { ... } while(0)konstrukcji breaki continuenadal działa wewnątrz danego bloku, ale ((void)0)tworzy błąd składniowy, jeśli średnik zostanie pominięty po wywołaniu makra, co w przeciwnym razie pominie następny blok. (W rzeczywistości nie ma tutaj problemu „wiszącego innego”, ponieważ elsewiąże się z najbliższym if, czyli tym w makrze.)

Jeśli interesują Cię rzeczy, które można wykonać mniej lub bardziej bezpiecznie za pomocą preprocesora C, sprawdź tę bibliotekę.


Choć jest bardzo sprytny, powoduje bombardowanie ostrzeżeniami kompilatora przed potencjalnym niebezpieczeństwem.
Segmentowany

1
Zwykle używasz makr do tworzenia zamkniętego środowiska, to znaczy nigdy nie używasz break(lub continue) wewnątrz makra do sterowania pętlą, która zaczyna się / kończy na zewnątrz, to po prostu zły styl i ukrywa potencjalne punkty wyjścia.
mirabilos

Istnieje również biblioteka preprocesora w Boost. Co w tym jest oszałamiającego?
Rene

Istnieje ryzyko, else ((void)0)że ktoś może pisać YOUR_MACRO(), f();i będzie poprawny pod względem składniowym, ale nigdy nie zadzwoni f(). Ze do whilejest to błąd składni.
melpomene

@melpomene, a co z tym else do; while (0)?
Carl Lei,

8

Z niektórych powodów nie mogę skomentować pierwszej odpowiedzi ...

Niektórzy z was pokazali makra ze zmiennymi lokalnymi, ale nikt nie wspomniał, że nie można po prostu użyć żadnej nazwy w makrze! Pewnego dnia ugryzie użytkownika! Dlaczego? Ponieważ argumenty wejściowe są podstawiane w szablonie makra. A w przykładach makr używasz prawdopodobnie najczęściej używanej zmiennej nazwy i .

Na przykład, gdy następujące makro

#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)

jest używany w następującej funkcji

void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

makro nie użyje zamierzonej zmiennej i, która jest zadeklarowana na początku some_func, ale zmienna lokalna, która jest zadeklarowana w pętli do ... while makra.

Dlatego nigdy nie używaj wspólnych nazw zmiennych w makrze!


Typowym wzorcem jest na przykład dodawanie znaków podkreślenia w nazwach zmiennych w makrach int __i;.
Blaisorblade,

8
@ Blaisorblade: W rzeczywistości jest to nieprawidłowe i nielegalne C; wiodące podkreślenia są zarezerwowane do wykorzystania przez implementację. Powodem, dla którego widziałeś ten „zwykły wzorzec”, jest odczytywanie nagłówków systemowych („implementacja”), które muszą ograniczać się do tej zarezerwowanej przestrzeni nazw. W przypadku aplikacji / bibliotek powinieneś wybrać własne niejasne, mało prawdopodobne kolizje nazw bez podkreśleń, np. mylib_internal___iLub podobnych.
R .. GitHub ZATRZYMAJ LÓD

2
@R .. Masz rację - faktycznie przeczytałem to w „aplikacji”, jądrze Linuksa, ale i tak jest to wyjątek, ponieważ nie korzysta ze standardowej biblioteki (technicznie rzecz biorąc, zamiast tego jest to wolnostojąca implementacja C jednego „hostowanego”).
Blaisorblade,

3
@R .. nie jest to do końca poprawne: wiodące podkreślenia, a następnie duże lub drugie podkreślenia są zarezerwowane do implementacji we wszystkich kontekstach. Wiodące podkreślenia, po których następuje coś innego, nie są zastrzeżone w zakresie lokalnym.
Leushenko

@Leushenko: Tak, ale rozróżnienie jest na tyle subtelne, że uważam, że najlepiej jest mówić ludziom, aby w ogóle nie używali takich nazw. Ludzie, którzy rozumieją tę subtelność, zapewne już wiedzą, że rozmyślam o szczegółach. :-)
R .. GitHub ZATRZYMAJ POMOC W LODZIE

6

Wyjaśnienie

do {} while (0)i if (1) {} elseupewnij się, że makro jest rozwinięte tylko do 1 instrukcji. Inaczej:

if (something)
  FOO(X); 

rozwinąłby się w:

if (something)
  f(X); g(X); 

I g(X)zostanie wykonany poza instrukcją ifsterującą. Można tego uniknąć podczas używania do {} while (0)iif (1) {} else .


Lepsza alternatywa

Dzięki wyrażeniu instrukcji GNU (nie będącym częścią standardowego C) masz lepszy sposób do {} while (0)i możesz to if (1) {} elserozwiązać, po prostu używając ({}):

#define FOO(X) ({f(X); g(X);})

Ta składnia jest zgodna z wartościami zwracanymi (zauważ, że do {} while (0)nie jest), jak w:

return FOO("X");

3
użycie blokowania blokowania {} w makrze byłoby wystarczające do powiązania kodu makra, aby wszystko zostało wykonane dla tej samej ścieżki warunku warunkowego. Czas dookoła służy do wymuszenia średnika w miejscach, w których makro jest używane. dlatego makro jest wymuszane, zachowując się bardziej podobnie. obejmuje to wymaganie końcowego średnika, gdy jest używany.
Alexander Stohr,

2

Nie sądzę, żeby o tym wspomniano, więc zastanów się nad tym

while(i<100)
  FOO(i++);

zostanie przetłumaczony na

while(i<100)
  do { f(i++); g(i++); } while (0)

zauważ, jak i++makro jest oceniane dwukrotnie. Może to prowadzić do interesujących błędów.


15
Nie ma to nic wspólnego z konstrukcją do ... while (0).
Trent

2
Prawdziwe. Ale dowiemy się z do tematu makr vs. funkcji i jak napisać makro, które zachowuje się jak funkcja ...
Jan Nilsson

2
Podobnie jak powyższy, nie jest to odpowiedź, ale komentarz. Na temat: dlatego używasz rzeczy tylko raz:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.