Neden makrolarda görünüşte anlamsız do-while ve if-else ifadeleri kullanıyorsunuz?


787

Birçok C / C ++ makroda anlamsız bir do whiledöngü gibi görünüyor içine sarılmış makro kodunu görüyorum . İşte örnekler.

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

Ne do whileyaptığını göremiyorum . Neden sadece onsuz yazmıyorsun?

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

2
Başka bir örnek voidiçin, sonuna türünde bir ifade eklerdim ... like ((void) 0) .
Phil1970

1
do whileYapının döndürme ifadeleriyle uyumlu olmadığını hatırlatmak için yapının if (1) { ... } else ((void)0)Standart C'de daha uyumlu kullanımları vardır. Ve GNU C'de, cevabımda açıklanan yapıyı tercih edersiniz.
Cœur

Yanıtlar:


829

do ... whileVe if ... elseo kadar Makronuz sonra noktalı virgül her zaman aynı anlama gelir olmak için vardır. Diyelim ki ikinci makronuz gibi bir şeyiniz var.

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

Şimdi , if ifadesinin gövdelerinin kıvırcık parantez içine alınmadığı BAR(X);bir if ... elseifadede kullanacak olsaydınız, kötü bir sürpriz elde edersiniz.

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

Yukarıdaki kod,

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

Bu sözdizimsel olarak yanlıştır, çünkü başka artık if ile ilişkili değildir. Makrodaki kıvırcık parantez içindeki şeyleri sarmaya yardımcı olmaz, çünkü parantezlerden sonra noktalı virgül sözdizimsel olarak yanlıştır.

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

Sorunu çözmenin iki yolu vardır. Birincisi, bir ifade gibi davranma yeteneğini soymadan makro içindeki ifadeleri sıralamak için virgül kullanmaktır.

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

Çubuğun yukarıdaki sürümü BAR, yukarıdaki kodu sözdizimsel olarak doğru olana genişletir.

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

Bunun yerine, f(X)örneğin yerel değişkenleri bildirmek için kendi bloğuna gitmesi gereken daha karmaşık bir kod gövdesine sahipseniz , bu işe yaramaz . En genel durumda çözüm, do ... whilemakronun karışıklık olmadan noktalı virgül alan tek bir ifade olmasına neden olmak gibi bir şey kullanmaktır .

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

Kullanmak zorunda değilsiniz, bir do ... whileşeyleri de pişirebilirsiniz if ... else, ancak bir if ... elseiçeride genişlediğinde , aşağıdaki kodda olduğu gibi mevcut bir sarkan başka problemi bulmayı daha da zorlaştırabilecek if ... elsebir " sarkan başka " ortaya çıkar. .

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

Nokta, noktalı virgülün, sarkan bir noktalı virgülün hatalı olduğu bağlamlarda kullanılmasıdır. Tabii ki, bu noktada BARbir makro değil gerçek bir işlev olarak ilan edilmesinin daha iyi olacağı iddia edilebilir (ve muhtemelen de) .

Özetle, do ... whileC ön işlemcisinin eksikliklerine çözüm bulmak için var. Bu C stil kılavuzları C ön işlemcisini bırakmanızı söylediğinde, bu endişe duydukları şeydir.


18
Bu, if, while ve ifadelerinde her zaman parantez kullanmak için güçlü bir argüman değil mi? Bunu her zaman yapmak için bir nokta yaparsanız (örneğin MISRA-C için gerekli olduğu gibi), yukarıda açıklanan sorun ortadan kalkar.
Steve Melnikoff

17
Virgül örneği olmalıdır, #define BAR(X) (f(X), g(X))aksi takdirde operatör önceliği anlambilimi bozabilir.
Stewart

20
@DawidFerenczy: siz ve ben dört buçuk yıl önceki iyi bir noktaya değinsek de, gerçek dünyada yaşamak zorundayız. Kodumuzdaki tüm ififadelerin vb. Parantez kullandığını garanti edemezsek, bunun gibi makroları sarmak sorunlardan kaçınmanın basit bir yoludur.
Steve Melnikoff

8
Not: if(1) {...} else void(0)form, do {...} while(0)parametreleri makro genişletmesine dahil edilen kod olan makrolar için daha güvenlidir , çünkü kesme veya devam anahtar kelimelerinin davranışını değiştirmez. Örneğin: mola, makro çağrı sitesindeki for döngüsü yerine makronun while döngüsünü etkilediği için tanımlandığında for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }beklenmedik davranışlara neden olur . MYMACRO#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)
Chris Kline

5
@ace void(0)bir yazım hatasıydı demek istedim (void)0. Ve ben buna inanıyorum gelmez hiçbir noktalı virgül sonra orada haber: "Başka sarkan" sorunu çözmek (void)0. Bu durumda başka bir sarkan şey (örn. if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) Bir derleme hatasını tetikler. İşte bunu gösteren canlı bir örnek
Chris Kline

153

Makrolar, ön işlemcinin orijinal koda koyacağı kopya / yapıştırılmış metin parçalarıdır; makronun yazarı, değiştirmenin geçerli kod üretmesini umuyor.

Bunu başarmak için üç iyi "ipucu" vardır:

Makronun orijinal kod gibi davranmasına yardımcı olun

Normal kod genellikle bir noktalı virgülle sonlandırılır. Kullanıcı kodu gerek duymazsa görüntüleyebilir ...

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

Bu, kullanıcının, noktalı virgül yoksa derleyiciden hata üretmesini beklediği anlamına gelir.

Ancak asıl gerçek neden, bir zamanlar, makronun yazarının makroyu belki de gerçek bir işlevle (belki de satır içi) değiştirmesi gerekeceğidir. Yani makro gerçekten bir gibi davranmalıdır.

Bu yüzden, yarı-kolon gerektiren bir makroya sahip olmalıyız.

Geçerli bir kod üret

Jfm3'ün cevabında gösterildiği gibi, bazen makro birden fazla talimat içerir. Makro bir if ifadesinin içinde kullanılıyorsa, bu sorunlu olacaktır:

if(bIsOk)
   MY_MACRO(42) ;

Bu makro şu şekilde genişletilebilir:

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

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

gFonksiyon bakılmaksızın değerinin çalıştırılacaktır bIsOk.

Bu, makroya bir kapsam eklememiz gerektiği anlamına gelir:

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

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

Geçerli bir kod üret 2

Makro şuna benzerse:

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

Aşağıdaki kodda başka bir sorunla karşılaşabiliriz:

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

Çünkü şu şekilde genişleyecektir:

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

Bu kod elbette derlenmeyecek. Yani, yine, çözüm bir kapsam kullanıyor:

#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 tekrar doğru şekilde çalışır.

Noktalı kolon + kapsam efektlerini birleştirmek?

Bu efekti üreten bir C / C ++ deyimi vardır: do / while döngüsü:

do
{
    // code
}
while(false) ;

Do / while, bir kapsam oluşturabilir, böylece makronun kodunu kapsülleyebilir ve sonunda bir yarım kolon gerektirir, böylece bir kod gerektiren kod haline gelir.

Bonus?

C ++ derleyicisi do / while döngüsünü optimize eder, çünkü post-koşulunun yanlış olması derleme zamanında bilinir. Bu, aşağıdaki gibi bir makro anlamına gelir:

#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.
}

doğru şekilde genişleyecek

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.
}

ve sonra derlenir ve

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

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

   // Etc.
}

6
Not satır içi işlev kişiliklerden için bazı standart ön tanımlı makro makro değişen, örneğin aşağıdaki kodun gösterdiği bir değişiklik olduğunu FUNCTION ve HATTI : #include <stdio.h> #define Fmacro () printf ( "% s% d \ n", İŞLEVİ , LINE ) satır içi geçersiz Finline () {printf ("% s% d \ n", FUNCTION , LINE ); int main () {Fmacro (); Finline (); dönüş 0; } (kalın terimler çift alt çizgi ile
çevrilmelidir

6
Bu cevapla ilgili birtakım ufak tefek ama tamamen önemsiz olmayan sorunlar vardır. Örneğin: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }doğru genişleme değildir; xgenişleme 32. Daha karmaşık bir konu genişlemesi budur olmalıdır MY_MACRO(i+7). Bir diğeri ise genişlemesi MY_MACRO(0x07 << 6). İyi olan çok şey var, ama bazı ibareli ve çaprazlanmamış t'ler var.
Jonathan Leffler

@Gnubie: Hala burada olduğunuzu ve bunu şimdiye kadar çözemediğinizi düşünüyorum: ters eğik çizgileri olan yorumlarda yıldız ve alt çizgilerden kaçabilirsiniz, bu yüzden yazarsanız \_\_LINE\_\___LINE__ olarak görüntülenir. IMHO, kod için kod biçimlendirmesi kullanmak daha iyidir; örneğin, __LINE__(herhangi bir özel işlem gerektirmez). PS: 2012 yılında bunun doğru olup olmadığını bilmiyorum; o zamandan beri motorda birkaç iyileştirme yaptılar.
Scott

1
Yorumumun altı yıl geciktiğini takdir etmekle birlikte, çoğu C derleyicisi aslında satır içi inlineişlevlere sahip değildir (standart tarafından izin verildiği gibi)
Andrew

53

@ jfm3 - Soruya güzel bir cevabın var. Ayrıca makro deyimin basit 'if' ifadeleriyle muhtemelen daha tehlikeli (hata olmadığı için) istenmeyen davranışı önlediğini de eklemek isteyebilirsiniz:

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

if (test) FOO( baz);

genişler:

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

bu sözdizimsel olarak doğrudur, bu nedenle derleyici hatası yoktur, ancak g () 'nin her zaman çağrılmasının muhtemelen istenmeyen bir sonucu vardır.


22
"muhtemelen istenmeyen"? "Kesinlikle istenmeyen" derdim, ya da başka bir programcı çıkarılması ve vurulması gerekiyor (bir kamçı ile yuvarlak olarak chastised aksine).
Lawrence Dol

4
Ya onlar üç harfli ajansı için çalışan ve gizlice yaygın olarak kullanılan açık kaynak programı ... :-) bu kodu ekleyerek eğer, zam verilsin diye
R .. GitHub DUR ICE YARDIMCI

2
Ve bu yorum bana Apple OS'lerde bulunan son SSL sertifikası doğrulama hatasındaki başarısızlık çizgisini hatırlatıyor
Gerard Sexton

23

Yukarıdaki cevaplar bu yapıların anlamını açıklar, ancak bahsedilmeyen ikisi arasında önemli bir fark vardır. Aslında, yapıyı tercih etmek do ... whileiçin bir neden var if ... else.

Sorunu if ... elseyapısının o olmamasıdır zorlamak noktalı virgül koymak sizi. Bu koddaki gibi:

FOO(1)
printf("abc");

Noktalı virgülü (yanlışlıkla) hariç tutsak da, kod

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

ve sessizce derlenecektir (bazı derleyiciler erişilemeyen kod için uyarı verebilir). Ancak printfifade asla yürütülmeyecek.

do ... whileYapının böyle bir sorunu yoktur, çünkü ondan sonraki tek geçerli simge while(0)noktalı virgüldür.


2
@RichardHansen: Hala o kadar iyi değil, çünkü makro çağrışımından bakıldığında bir ifadeye mi yoksa bir ifadeye mi genişlediğini bilmiyorsunuz. Birisi daha sonra varsayarsa, FOO(1),x++;yine bize yanlış bir pozitif verecek yazabilir . Sadece kullan do ... whileve hepsi bu.
Yakov Galka

1
Yanlış anlaşılmayı önlemek için makroyu belgelemek yeterlidir. Ben do ... while (0)tercih edilir, ama bir dezavantajı var: A breakveya döngü, makro çağırma içeren döngü değil continuekontrol edecek do ... while (0). Yani ifhile hala değerlidir.
Richard Hansen

2
Makroları sözde döngünüzün içinde olduğu gibi bir breakveya bir nereye koyabileceğinizi görmüyorum . Macro parametresinde bile bir sözdizimi hatası yapar. continuedo {...} while(0)
Patrick Schlüter

5
Yapı do { ... } while(0)yerine kullanmanın bir diğer nedeni if whatever, onun deyimsel doğasıdır. do {...} while(0)Yapı, iyi yaygın bilinen ve birçok programcı tarafından çok kullanılır. Gerekçesi ve dokümantasyonu kolaylıkla bilinmektedir. ifYapı için öyle değil . Bu nedenle kod incelemesi yaparken grok yapmak daha az çaba gerektirir.
Patrick Schlüter

1
@ tristopia: İnsanların kod bloklarını argüman olarak alan makrolar yazdıklarını gördüm (ki mutlaka tavsiye etmiyorum). Örneğin: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Bu gibi kullanılabilir CHECK(system("foo"), break;);burada, break;kapama döngü ifade eder CHECK()çağırma.
Richard Hansen

16

Derleyicilerin do { ... } while(false);döngüleri optimize etmeleri beklenirken, bu yapıyı gerektirmeyen başka bir çözüm daha vardır. Çözüm, virgül operatörünü kullanmaktır:

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

hatta daha egzotik:

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

Bu, ayrı talimatlarla iyi çalışsa da, değişkenlerin aşağıdakilerin bir parçası olarak oluşturulduğu ve kullanıldığı durumlarda işe yaramaz #define:

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

Bununla do / while yapısını kullanmak zorunda kalacaktır.


teşekkürler, virgül operatörü yürütme sırasını garanti etmediğinden, bu yuvalama bunu zorlamanın bir yoludur.
Marius

15
@Marius: Yanlış; virgül operatörü bir sekans noktasıdır ve bu şekilde yok garanti yürütme düzeni. Fonksiyon argüman listelerinde virgülle karıştırdığından şüpheleniyorum.
R .. GitHub DURDURMAK BUZA YARDIM

2
İkinci egzotik öneri günümü yaptı.
Spidey

Sadece derleyiciler program gözlemlenebilir davranışı korumak zorunda olduğunu eklemek istedim, bu yüzden do / while optimize optimize çok önemli değil (derleyici optimizasyon doğru olduğunu varsayarak).
Marco

@MarcoA. Doğru olsanız da, geçmişte derleyici optimizasyonunun kod işlevini tam olarak korurken bulmuştum, ancak tekil bağlamda hiçbir şey yapamayacak gibi görünen satırları değiştirerek çok iş parçacıklı algoritmaları kıracak. Vaka noktası Peterson's Algorithm.
Marius

11

Jens Gustedt'ın P99 önişlemci kütüphanesi (evet, böyle bir şeyin olması aklımı da havaya uçurdu!) if(1) { ... } elseAşağıdakileri tanımlayarak yapıyı küçük ama önemli bir şekilde geliştirir:

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

Bunun mantığı, do { ... } while(0)yapıdan farklı olarak breakve continueyine de verilen blok içinde çalışır, ancak ((void)0)makro çağrısından sonra noktalı virgül atlanırsa bir sonraki bloğu atlayacak olan bir sözdizimi hatası oluşturur. (Aslında burada "sarkan başka bir sorun" yok, çünküelse makrodaki en yakın olana bağlanır if.)

C ön işlemcisi ile az ya da çok güvenle yapılabilecek şeylerle ilgileniyorsanız, bu kütüphaneye göz atın.


Çok zeki olsa da, bu durum potansiyel sarkma konusunda derleyici uyarılarıyla bombalanmasına neden olur.
Segment

1
İçerilen bir ortam oluşturmak için genellikle makroları kullanırsınız, yani dışarıda başlayan / biten bir döngüyü kontrol etmek için hiçbir zaman bir makronun içinde break(veya continue) kullanmazsınız , bu sadece kötü bir stildir ve potansiyel çıkış noktalarını gizler.
mirabilos

Ayrıca Boost'ta bir önişlemci kütüphanesi var. Bu konuda akıllara durgunluk veren nedir?
Rene

Risk else ((void)0), birisinin yazıyor olabileceği YOUR_MACRO(), f();ve sözdizimsel olarak geçerli olacağı, ancak asla aramayacağıdır f(). Bu do whilebir sözdizimi hatası.
Melpomene

@ nasıl yani else do; while (0)?
Carl Lei

8

Bazı nedenlerden dolayı ilk cevap hakkında yorum yapamıyorum ...

Bazılarınız yerel değişkenlere sahip makrolar gösterdiniz, ancak kimse makroda herhangi bir isim kullanamayacağınızı söylemedi! Bir gün kullanıcıyı ısırır! Neden? Çünkü giriş argümanları makro şablonunuzla değiştirilir. Makro örneklerinizde, en yaygın olarak kullanılan değişken i adını kullanırsınız .

Örneğin, aşağıdaki makro

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

aşağıdaki işlevde kullanılır

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

makro, some_func öğesinin başında bildirilen i değişkenini değil, makronun do ... while döngüsünde bildirilen yerel değişkeni kullanmaz.

Bu nedenle, hiçbir zaman bir makroda ortak değişken adları kullanmayın!


Genel desen, örneğin makrolardaki değişken adlarına alt çizgi eklemektir int __i;.
Blaisorblade

8
@Blaisorblade: Aslında bu yanlış ve yasadışı C; önde gelen alt çizgiler uygulama tarafından kullanılmak üzere ayrılmıştır. Bu "olağan modeli" görmenizin nedeni, kendilerini bu ayrılmış ad alanı ile sınırlaması gereken sistem başlıklarını ("uygulama") okumaktır. Uygulamalar / kütüphaneler için, altçizgi olmayan, ör. mylib_internal___iVeya benzeri , belirsiz, çarpışması olası olmayan adlarınızı seçmelisiniz .
R .. GitHub DURDURMAK BUZA YARDIM

2
@R .. Haklısın - Bunu aslında bir '' uygulama '', Linux çekirdeği içinde okudum, ancak standart bir kitaplık kullanmadığı için yine de bir istisna (teknik olarak, bir '' bağımsız '' C uygulaması '' barındırılan '' bir).
Blaisorblade

3
@R .. bu pek doğru değil: baştaki alt çizgiler ve ardından bir alt çizgi veya ikinci alt çizgi tüm bağlamlarda uygulama için ayrılmıştır. Başlıca alt çizgiler ve ardından başka bir şey yerel kapsamda ayrılmaz.
Leushenko

@Leushenko: Evet, ama ayrım yeterince incedir ki insanlara bu tür isimleri kullanmamalarını en iyi şekilde buluyorum. İnceliği anlayan insanlar muhtemelen ayrıntılar üzerinde parladığımı zaten biliyorlar. :-)
R .. GitHub BUZA YARDIM ETMEYİN

7

açıklama

do {} while (0)ve if (1) {} elsemakronun yalnızca 1 talimata genişletildiğinden emin olmak içindir. Aksi takdirde:

if (something)
  FOO(X); 

genişler:

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

Ve kontrol ifadesi g(X)dışında yürütülür if. Bu kullanırken önlenir do {} while (0)ve if (1) {} else.


Daha iyi bir alternatif

Bir GNU deyimi ifadesiyle (standart C'nin bir parçası değil), bunu kullanarak do {} while (0)ve if (1) {} elsebunu çözmekten daha iyi bir yolunuz vardır ({}):

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

Ve bu sözdizimi, aşağıdaki gibi dönüş değerleri (not do {} while (0)not) ile uyumludur :

return FOO("X");

3
makroda blok bağlamanın {} kullanılması, makro kodunun gruplanması için yeterli olur, böylece hepsi aynı if koşulu yolu için yürütülür. do-while around, makronun kullanıldığı yerlerde noktalı virgül uygulamak için kullanılır. böylece makro daha çok işlev görür. bu, kullanıldığında sondaki noktalı virgül gereksinimini içerir.
Alexander Stohr

2

Bahsettiğini sanmıyorum, bu yüzden şunu düşün

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

çevrilecek

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

i++makro tarafından iki kez nasıl değerlendirildiğine dikkat edin. Bu bazı ilginç hatalara yol açabilir.


15
Bunun do ... while (0) yapısı ile ilgisi yoktur.
Trent

2
Doğru. Ancak makrolar ve işlevler konusuna ve işlev olarak işlev gören bir makronun nasıl yazılacağına bağlı ...
John Nilsson

2
Yukarıdakine benzer şekilde, bu bir cevap değil, bir yorumdur. Konu hakkında: Bu yüzden sadece bir kez şeyler kullanıyorsunuz:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.