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

W wielu makrach C / C++ widzę kod makra zawinięty w coś, co wydaje się być bezsensowną pętlą do while. 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 robi do while. Dlaczego nie napisać tego bez tego?
#define FOO(X) f(X); g(X)
Author: Lundin, 2008-09-30

10 answers

do ... while i if ... else są po to, aby średnik po makrze zawsze oznacza to samo. Powiedzmy, że ty miałem coś takiego jak twoje drugie makro.

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

Teraz, jeśli miałbyś użyć BAR(X); w if ... else deklaracji, gdzie ciała deklaracji if nie były owinięte nawiasami klamrowymi, miałbyś złą niespodziankę.

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

Powyższy kod rozszerzyłby się na

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

Co jest niepoprawne składniowo, ponieważ else nie jest już związane z if. Nie pomaga zawijanie rzeczy w nawiasach klamrowych wewnątrz makra, ponieważ średnik po nawiasach jest niepoprawny składniowo.

if (corge)
  {f(corge); g(corge);};
else
  gralt();
Istnieją dwa sposoby rozwiązania problemu. Pierwszym z nich jest użycie przecinka do sekwencji wyrażeń w makrze bez pozbawiania go zdolności do działania jak wyrażenie.
#define BAR(X) f(X), g(X)

Powyższa wersja bar BAR rozszerza powyższy kod do tego, co następuje, co jest poprawne składniowo.

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

To nie działa, jeśli zamiast z f(X) masz bardziej skomplikowany kod, który musi przejść we własnym bloku, na przykład do deklarowania zmiennych lokalnych. W najbardziej ogólnym przypadku rozwiązaniem jest użycie czegoś w rodzaju do ... while, aby makro było pojedynczą instrukcją, która przyjmuje średnik bez nieporozumień.

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

Nie musisz używać do ... while, Możesz również ugotować coś z if ... else, chociaż gdy if ... else rozszerza się wewnątrz if ... else prowadzi do " zwisającego else ", co może sprawić, że istniejący problem zwisający jeszcze trudniej znaleźć, jak w poniższym kodzie.

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

Chodzi o użycie średnika w kontekstach, w których zwisający średnik jest błędny. Oczywiście w tym miejscu można (i prawdopodobnie powinno) argumentować, że lepiej zadeklarować BAR jako rzeczywistą funkcję, a nie makro.

Podsumowując, do ... while jest po to, aby obejść niedociągnięcia preprocesora C. Kiedy te podręczniki stylu C każą Ci zwolnić preprocesor C, właśnie o to się martwią.

 731
Author: jfm3,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-15 18:26:38

Makra są kopiowanymi / wklejanymi fragmentami tekstu, które pre-procesor umieści w oryginalnym kodzie; autor makra ma nadzieję, że zastąpienie stworzy poprawny kod.

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

Pomóż makro zachowywać się jak prawdziwy kod

Zwykły kod jest zwykle zakończony dwukropkiem. Jeśli użytkownik przegląda 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 wytworzy 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 inlined). Więc makro powinno naprawdę zachowywać się jak jedno.

Więc powinniśmy mieć makro wymagające dwukropka.

Wygeneruj prawidłowy kod

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

if(bIsOk)
   MY_MACRO(42) ;

To makro można rozszerzyć jako:

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

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

Funkcja g 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) ;

Podaj prawidłowy kod 2

Jeśli makro jest takie jak:

#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ż rozszerzyłoby się jako:

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

Ten kod nie będzie kompilowany, oczywiście. Więc, ponownie, rozwiązanie jest za pomocą zakresu:

#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 zachowuje się ponownie poprawnie.

Łączenie efektów półkropka + lunety?

Istnieje jeden idiom C / C++, który wywołuje ten efekt: pętla do/while:
do
{
    // code
}
while(false) ;

Do / while może utworzyć zakres, zamykając w ten sposób kod makra, a na końcu potrzebuje dwukropka, rozszerzając go do kodu potrzebującego.

Bonus?

Kompilator C++ zoptymalizuje pętlę do / while, ponieważ jego post-condition is false 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.
}
 136
Author: paercebal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-13 08:31:21

@Jfm3 - masz miłą odpowiedź na pytanie. Możesz też dodać, że idiom makro zapobiega również prawdopodobnie bardziej niebezpiecznemu (ponieważ nie ma błędu) niezamierzonemu zachowaniu z prostymi instrukcjami "if":

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

if (test) FOO( baz);

Rozszerza się do:

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

Który jest poprawny składniowo, więc nie ma błędu kompilatora, ale ma prawdopodobnie niezamierzoną konsekwencję, że g() będzie zawsze wywoływane.

 50
Author: Michael Burr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-30 18:05:30

Powyższe odpowiedzi wyjaśniają znaczenie tych konstrukcji, ale istnieje znacząca różnica między nimi, która nie została wymieniona. W rzeczywistości istnieje powód, aby preferować do ... while do if ... else konstrukcji.

Problem konstrukcji if ... else polega na tym, że nie zmusza ona do umieszczenia średnika. Jak w tym kodzie:

FOO(1)
printf("abc");

Chociaż pominęliśmy średnik (przez pomyłkę), kod rozszerzy się do

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

I po cichu się skompiluje (chociaż niektóre Kompilatory mogą wystawić ostrzeżenie o nieosiągalnym kodzie). Ale printf oświadczenie nigdy nie zostanie wykonane.

do ... while construct nie ma takiego problemu, ponieważ jedynym poprawnym tokenem po while(0) jest średnik.

 21
Author: ybungalobill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-03 15:21:08

Chociaż oczekuje się, że Kompilatory optymalizują pętle do { ... } while(false);, istnieje inne rozwiązanie, które nie wymagałoby tego konstruktu. Rozwiązaniem jest użycie operatora przecinka:

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

Lub jeszcze bardziej egzotycznie:

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

Chociaż będzie to dobrze działać z oddzielnymi 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))

Z tym trzeba by użyć konstruktu do/while.

 15
Author: Marius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-10 08:33:06

Biblioteka preprocesora Jensa Gustedta P99 (tak, fakt, że taka rzecz istnieje, rozwalił też mój umysł!) ulepsza konstrukcję {[1] } w sposób mały, ale znaczący, 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 jest to, że w przeciwieństwie do konstrukcji do { ... } while(0), break i continue nadal działają wewnątrz danego bloku, ale ((void)0) tworzy błąd składni, jeśli średnik zostanie pominięty po wywołaniu makra, co w przeciwnym razie pominie następny blok. (Właściwie nie ma problem "zwisania" tutaj, ponieważ else wiąże się z najbliższym if, czyli tym w makrze.)

Jeśli interesują Cię rzeczy, które można zrobić mniej lub bardziej bezpiecznie z preprocesorem C, sprawdź tę bibliotekę.

 8
Author: Isaac Schwabacher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-19 16:19:45

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

Niektórzy z was pokazali makra ze zmiennymi lokalnymi, ale nikt nie wspomniał, że nie można używać żadnej nazwy w makrze! Kiedyś ugryzie użytkownika! Dlaczego? Ponieważ argumenty wejściowe są podstawione do szablonu makra. W przykładach makr użyłeś prawdopodobnie najczęściej używanej zmiennej 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 stosowany w następujących function

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 zmiennej lokalnej, która jest zadeklarowana w do ... pętla while makra.

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

 6
Author: Mike Meyer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-21 18:42:56

Nie wydaje mi się, żeby to było wspomniane, więc rozważ to

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

Zostanie przetłumaczone na

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

Zauważ, jak i++ jest oceniane dwukrotnie przez makro. Może to prowadzić do ciekawych błędów.

 3
Author: John Nilsson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-18 22:04:41

Uważam, że ta sztuczka jest bardzo pomocna w sytuacjach, w których musisz sekwencyjnie przetwarzać określoną wartość. Na każdym poziomie przetwarzania, jeśli wystąpi jakiś błąd lub nieprawidłowy warunek, można uniknąć dalszego przetwarzania i przerwać wcześnie. np.

#define CALL_AND_RETURN(x)  if ( x() == false) break;
do {
     CALL_AND_RETURN(process_first);
     CALL_AND_RETURN(process_second);
     CALL_AND_RETURN(process_third);
     //(simply add other calls here)
} while (0);
 1
Author: pankaj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-18 04:34:54

Wyjaśnienie

do {} while (0) i if (1) {} else mają upewnić się, że makro jest rozszerzone do tylko 1 instrukcji. Inaczej:

if (something)
  FOO(X); 

Rozszerzy do:

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

I g(X) będą wykonywane poza if instrukcją kontrolną. Unika się tego podczas stosowania do {} while (0) i if (1) {} else.


Better alternative

Z wyrażeniem instrukcji GNU (nie jest częścią standardu C), masz lepszy sposób niż do {} while (0) i if (1) {} else, aby rozwiązać ten problem, po prostu używając ({}):

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

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

return FOO("X");
 1
Author: Cœur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-05 05:32:28