Mengapa menggunakan pernyataan do-while dan if-else yang tampaknya tidak berarti di makro?

788

Dalam banyak makro C / C ++ saya melihat kode makro dibungkus dalam apa yang tampak seperti do whileloop yang tidak berarti . Berikut ini contohnya.

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

Saya tidak bisa melihat apa yang do whiledilakukannya. Mengapa tidak menulis saja tanpa itu?

#define FOO(X) f(X); g(X)
jfm3
sumber
2
Sebagai contoh dengan yang lain, saya akan menambahkan ekspresi voidtipe di akhir ... like ((void) 0) .
Phil1970
1
Ingatkan bahwa do whilekonstruk tidak kompatibel dengan pernyataan pengembalian, sehingga if (1) { ... } else ((void)0)konstruk tersebut memiliki lebih banyak penggunaan yang kompatibel dalam Standar C. Dan di GNU C, Anda akan lebih suka konstruk yang dijelaskan dalam jawaban saya.
Cœur

Jawaban:

829

The do ... whiledan if ... elseada untuk membuatnya sehingga titik koma setelah makro Anda selalu berarti hal yang sama. Katakanlah Anda memiliki sesuatu seperti makro kedua Anda.

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

Sekarang jika Anda menggunakannya BAR(X);dalam sebuah if ... elsepernyataan, di mana tubuh pernyataan if tidak dibungkus dengan kurung keriting, Anda akan mendapatkan kejutan buruk.

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

Kode di atas akan berkembang menjadi

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

yang secara sintaksis salah, karena yang lain tidak lagi dikaitkan dengan if. Tidak membantu untuk membungkus barang-barang dengan kurung kurawal di dalam makro, karena titik koma setelah kurung kurawal secara sintaksis salah.

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

Ada dua cara untuk memperbaiki masalah. Yang pertama adalah menggunakan koma untuk mengurutkan pernyataan dalam makro tanpa merampasnya dari kemampuannya untuk bertindak seperti ekspresi.

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

Versi bar BARdi atas memperluas kode di atas menjadi apa yang berikut, yang secara sintaksis benar.

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

Ini tidak berfungsi jika alih-alih f(X)Anda memiliki kumpulan kode yang lebih rumit yang perlu dimasukkan dalam bloknya sendiri, katakan misalnya untuk mendeklarasikan variabel lokal. Dalam kasus yang paling umum solusinya adalah menggunakan sesuatu seperti do ... whilemenyebabkan makro menjadi pernyataan tunggal yang mengambil titik koma tanpa kebingungan.

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

Anda tidak harus menggunakan do ... while, Anda bisa memasak sesuatu dengan if ... elsebaik, meskipun ketika if ... elsemengembang di dalam if ... elseitu mengarah ke " menggantung lain ", yang bisa membuat masalah menggantung lain yang ada semakin sulit ditemukan, seperti dalam kode berikut .

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

Intinya adalah untuk menggunakan titik koma dalam konteks di mana titik koma menggantung salah. Tentu saja, pada saat ini dapat (dan mungkin harus) diperdebatkan bahwa akan lebih baik untuk menyatakan BARsebagai fungsi aktual, bukan makro.

Singkatnya, do ... whileapakah ada untuk mengatasi kekurangan preprocessor C. Ketika pemandu gaya C memberitahu Anda untuk memberhentikan preprosesor C, ini adalah jenis hal yang mereka khawatirkan.

jfm3
sumber
18
Bukankah ini argumen yang kuat untuk selalu menggunakan kawat gigi di if, while dan for statement? Jika Anda ingin selalu melakukan ini (seperti yang diperlukan untuk MISRA-C, misalnya), masalah yang dijelaskan di atas hilang.
Steve Melnikoff
17
Contoh koma harusnya #define BAR(X) (f(X), g(X))kalau bukan operator diutamakan dapat mengacaukan semantik.
Stewart
20
@DawidFerenczy: meskipun Anda dan saya-dari-empat-dan-setengah tahun yang lalu membuat poin yang baik, kita harus hidup di dunia nyata. Kecuali jika kami dapat menjamin bahwa semua ifpernyataan, dll, dalam kode kami menggunakan kawat gigi, maka membungkus makro seperti ini adalah cara sederhana untuk menghindari masalah.
Steve Melnikoff
8
Catatan: if(1) {...} else void(0)formulir lebih aman daripada do {...} while(0)makro untuk yang parameternya adalah kode yang termasuk dalam ekspansi makro, karena tidak mengubah perilaku break atau melanjutkan kata kunci. Sebagai contoh: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }menyebabkan perilaku yang tidak terduga ketika MYMACROdidefinisikan sebagai #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)karena istirahat mempengaruhi loop sementara makro daripada loop untuk di situs panggilan makro.
Chris Kline
5
@ace void(0)adalah kesalahan ketik, maksudku (void)0. Dan saya percaya ini tidak menyelesaikan masalah "menggantung lain": perhatikan tidak ada titik koma setelah (void)0. Yang lain tergantung dalam kasus itu (misalnya if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) memicu kesalahan kompilasi. Inilah contoh langsung yang menunjukkannya
Chris Kline
153

Macro adalah copy / paste teks yang akan dimasukkan oleh pre-processor ke dalam kode asli; penulis makro berharap penggantian akan menghasilkan kode yang valid.

Ada tiga "tips" yang baik untuk berhasil dalam hal itu:

Bantu makro berperilaku seperti kode asli

Kode normal biasanya diakhiri dengan titik koma. Haruskah kode tampilan pengguna tidak memerlukan satu ...

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

Ini berarti pengguna mengharapkan kompiler untuk menghasilkan kesalahan jika titik koma tidak ada.

Tetapi alasan sebenarnya yang sangat bagus adalah bahwa pada suatu saat, penulis makro mungkin perlu mengganti makro dengan fungsi asli (mungkin diuraikan). Jadi makro harus benar - benar berperilaku seperti itu.

Jadi kita harus memiliki makro yang membutuhkan semi-colon.

Menghasilkan kode yang valid

Seperti yang ditunjukkan pada jawaban jfm3, terkadang makro berisi lebih dari satu instruksi. Dan jika makro digunakan di dalam pernyataan if, ini akan bermasalah:

if(bIsOk)
   MY_MACRO(42) ;

Makro ini dapat diperluas sebagai:

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

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

The gfungsi akan dieksekusi terlepas dari nilai bIsOk.

Ini berarti bahwa kita harus menambahkan ruang lingkup ke makro:

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

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

Menghasilkan kode yang valid 2

Jika makro adalah sesuatu seperti:

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

Kami dapat memiliki masalah lain dalam kode berikut:

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

Karena itu akan berkembang sebagai:

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

Kode ini tidak akan dikompilasi, tentu saja. Jadi, sekali lagi, solusinya menggunakan cakupan:

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

Kode berperilaku dengan benar lagi.

Menggabungkan efek semi-colon + scope?

Ada satu idiom C / C ++ yang menghasilkan efek ini: Loop do / while:

do
{
    // code
}
while(false) ;

Do / while dapat membuat ruang lingkup, dengan demikian merangkum kode makro, dan membutuhkan semi-colon pada akhirnya, sehingga berkembang menjadi kode yang membutuhkannya.

Bonusnya?

Compiler C ++ akan mengoptimalkan loop do / while, karena post-conditionnya salah diketahui pada waktu kompilasi. Ini berarti makro seperti:

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

akan berkembang dengan benar sebagai

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

dan kemudian dikompilasi dan dioptimalkan sebagai

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

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

   // Etc.
}
paercebal
sumber
6
Perhatikan bahwa mengubah makro menjadi fungsi inline mengubah beberapa makro standar yang telah ditentukan sebelumnya, misalnya kode berikut menunjukkan perubahan FUNGSI dan LINE : #sertakan <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); return 0; } (istilah tebal harus dilampirkan oleh dua garis bawah - formatter buruk!)
Gnubie
6
Ada sejumlah masalah kecil tetapi tidak sepenuhnya ngawur dengan jawaban ini. Misalnya: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }bukan ekspansi yang benar; yang xdalam ekspansi harus 32. Sebuah masalah yang lebih kompleks adalah apa adalah perluasan MY_MACRO(i+7). Dan yang lainnya adalah perluasan MY_MACRO(0x07 << 6). Ada banyak hal yang baik, tetapi ada beberapa huruf 'undotted' dan 't unrossed.
Jonathan Leffler
@Gnubie: Saya perkirakan Anda masih di sini dan Anda belum mengetahuinya sekarang: Anda dapat melarikan diri tanda bintang dan garis bawah dalam komentar dengan garis miring terbalik, jadi jika Anda mengetiknya diterjemahkan \_\_LINE\_\_sebagai __LINE__. IMHO, lebih baik hanya menggunakan pemformatan kode untuk kode; misalnya, __LINE__(yang tidak memerlukan penanganan khusus). PS Saya tidak tahu apakah ini benar pada 2012; mereka telah membuat beberapa perbaikan pada mesin sejak itu.
Scott
1
Menghargai komentar saya yang terlambat enam tahun, tetapi kebanyakan kompiler C sebenarnya tidak memiliki inlinefungsi sebaris (seperti yang diizinkan oleh standar)
Andrew
53

@ jfm3 - Anda punya jawaban yang bagus untuk pertanyaan itu. Anda mungkin juga ingin menambahkan bahwa idiom makro juga mencegah perilaku yang mungkin lebih berbahaya (karena tidak ada kesalahan) dengan pernyataan 'jika' yang sederhana:

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

if (test) FOO( baz);

memperluas ke:

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

yang secara sintaksis benar sehingga tidak ada kesalahan kompiler, tetapi memiliki konsekuensi yang mungkin tidak diinginkan yaitu g () akan selalu dipanggil.

Michael Burr
sumber
22
"mungkin tidak disengaja"? Saya akan mengatakan "tentu saja tidak disengaja", atau programmer harus dikeluarkan dan ditembak (sebagai lawan secara kasar dihukum dengan cambuk).
Lawrence Dol
4
Atau mereka mungkin diberi kenaikan gaji, jika mereka bekerja untuk agensi tiga huruf dan secara diam-diam memasukkan kode itu ke dalam program open source yang banyak digunakan ... :-)
R .. GitHub BERHENTI MENGHENTIKAN ICE
2
Dan komentar ini hanya mengingatkan saya pada garis kegagalan goto dalam bug verifikasi sertifikat SSL baru-baru ini yang ditemukan di OS Apple
Gerard Sexton
23

Jawaban di atas menjelaskan arti dari konstruksi ini, tetapi ada perbedaan yang signifikan antara keduanya yang tidak disebutkan. Bahkan, ada alasan untuk lebih memilih do ... whiledaripada if ... elsemembangun.

Masalah if ... elsekonstruknya adalah bahwa itu tidak memaksa Anda untuk meletakkan titik koma. Seperti dalam kode ini:

FOO(1)
printf("abc");

Meskipun kami meninggalkan titik koma (karena kesalahan), kode akan diperluas ke

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

dan akan mengkompilasi secara diam-diam (meskipun beberapa kompiler dapat mengeluarkan peringatan untuk kode yang tidak terjangkau). Namun printfpernyataan itu tidak akan pernah dieksekusi.

do ... whilekonstruk tidak memiliki masalah seperti itu, karena satu-satunya token yang valid setelah while(0)adalah titik koma.

Yakov Galka
sumber
2
@RichardHansen: Masih tidak sebagus ini, karena dari tampilan permintaan makro Anda tidak tahu apakah itu diperluas ke pernyataan atau ekspresi. Jika seseorang mengasumsikan nanti, dia dapat menulis FOO(1),x++;yang lagi akan memberi kita positif palsu. Cukup gunakan do ... whiledan hanya itu.
Yakov Galka
1
Mendokumentasikan makro untuk menghindari kesalahpahaman harus cukup. Saya setuju bahwa do ... while (0)itu lebih disukai, tetapi memiliki satu kelemahan: A breakatau continueakan mengontrol do ... while (0)loop, bukan loop yang berisi permintaan makro. Jadi iftriknya masih memiliki nilai.
Richard Hansen
2
Saya tidak melihat di mana Anda bisa meletakkan breakatau continueyang akan dilihat sebagai di dalam macro do {...} while(0)pseudo-loop Anda. Bahkan dalam parameter makro itu akan membuat kesalahan sintaksis.
Patrick Schlüter
5
Alasan lain untuk menggunakan do { ... } while(0)alih-alih if whatevermembangun, adalah sifat idiomatis dari itu. The do {...} while(0)konstruk luas, terkenal dan banyak digunakan oleh banyak programmer. Dasar pemikiran dan dokumentasinya mudah diketahui. Tidak demikian halnya dengan ifkonstruknya. Oleh karena itu dibutuhkan sedikit usaha untuk grok ketika melakukan peninjauan kode.
Patrick Schlüter
1
@tristopia: Saya telah melihat orang-orang menulis macro yang mengambil blok kode sebagai argumen (yang tidak saya sarankan). Sebagai contoh: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Itu bisa digunakan seperti CHECK(system("foo"), break;);, di mana break;dimaksudkan untuk merujuk ke loop melampirkan CHECK()doa.
Richard Hansen
16

Meskipun diharapkan kompiler mengoptimalkan do { ... } while(false);loop, ada solusi lain yang tidak memerlukan konstruksi itu. Solusinya adalah menggunakan operator koma:

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

atau bahkan lebih eksotis:

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

Meskipun ini akan bekerja dengan baik dengan instruksi terpisah, itu tidak akan bekerja dengan kasus-kasus di mana variabel dibangun dan digunakan sebagai bagian dari #define:

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

Dengan ini akan dipaksa untuk menggunakan do / while build.

Marius
sumber
terima kasih, karena operator koma tidak menjamin pesanan eksekusi, penyatuan ini adalah cara untuk menegakkannya.
Marius
15
@Marius: Salah; operator koma adalah titik berurutan dan dengan demikian tidak perintah eksekusi jaminan. Saya curiga Anda bingung dengan koma dalam daftar argumen fungsi.
R .. GitHub BERHENTI MEMBANTU ICE
2
Saran eksotis kedua membuat hari saya.
Spidey
Hanya ingin menambahkan bahwa kompiler dipaksa untuk melestarikan perilaku program yang dapat diamati sehingga mengoptimalkan do / sementara pergi tidak banyak masalah besar (dengan asumsi optimisasi kompiler sudah benar).
Marco A.
@MarcoA. sementara Anda benar, saya telah menemukan di masa lalu bahwa optimasi kompiler, sambil tetap mempertahankan fungsi kode, tetapi dengan mengubah garis-garis yang tampaknya tidak melakukan apa pun dalam konteks tunggal, akan memecah algoritma multithreaded. Contoh kasus Peterson's Algorithm.
Marius
11

P99 preprocessor library Jens Gustedt (ya, fakta bahwa hal seperti itu ada di pikiranku juga!) Meningkatkan if(1) { ... } elsekonstruk dengan cara yang kecil tapi signifikan dengan mendefinisikan yang berikut:

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

Alasan untuk ini adalah bahwa, tidak seperti do { ... } while(0)konstruk, breakdan continuemasih bekerja di dalam blok yang diberikan, tetapi ((void)0)menciptakan kesalahan sintaksis jika titik koma dihilangkan setelah panggilan makro, yang sebaliknya akan melewati blok berikutnya. (Sebenarnya tidak ada masalah "menggantung yang lain" di sini, karena elsemengikat ke yang terdekat if, yang merupakan masalah di makro.)

Jika Anda tertarik pada hal-hal yang dapat dilakukan lebih atau kurang aman dengan preproses C, periksa perpustakaan itu.

Isaac Schwabacher
sumber
Meskipun sangat pintar, ini menyebabkan seseorang dibombardir dengan peringatan kompiler tentang kemungkinan menggantung lainnya.
Tersegmentasi
1
Anda biasanya menggunakan makro untuk membuat lingkungan yang terkandung, yaitu, Anda tidak pernah menggunakan break(atau continue) di dalam makro untuk mengontrol loop yang dimulai / berakhir di luar, itu hanya gaya yang buruk dan menyembunyikan titik keluar potensial.
mirabilos
Ada juga perpustakaan preprocessor di Boost. Apa yang mengejutkan tentang hal itu?
Rene
Risikonya else ((void)0)adalah seseorang mungkin menulis YOUR_MACRO(), f();dan secara sintaksis valid, tetapi tidak pernah menelepon f(). Dengan do whilekesalahan sintaks.
melpomene
@melpomene jadi bagaimana dengan else do; while (0)?
Carl Lei
8

Untuk beberapa alasan saya tidak dapat mengomentari jawaban pertama ...

Beberapa dari Anda menunjukkan makro dengan variabel lokal, tetapi tidak ada yang menyebutkan bahwa Anda tidak bisa menggunakan nama apa pun di makro! Ini akan menggigit pengguna suatu hari! Mengapa? Karena argumen input diganti ke template makro Anda. Dan dalam contoh makro Anda, Anda telah menggunakan nama variabel i yang mungkin paling sering digunakan .

Misalnya saat makro berikut

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

digunakan dalam fungsi berikut

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

makro tidak akan menggunakan variabel i yang dimaksudkan, yang dideklarasikan di awal some_func, tetapi variabel lokal, yang dideklarasikan di do ... while di makro.

Jadi, jangan pernah menggunakan nama variabel umum dalam makro!

Mike Meyer
sumber
Pola yang biasa adalah menambahkan garis bawah pada nama variabel di makro - misalnya int __i;.
Blaisorblade
8
@ Blaisorblade: Sebenarnya itu tidak benar dan ilegal C; garis bawah utama disediakan untuk digunakan oleh implementasi. Alasan Anda telah melihat "pola biasa" ini adalah dari membaca tajuk sistem ("implementasi") yang harus membatasi diri ke namespace yang dipesan ini. Untuk aplikasi / perpustakaan, Anda harus memilih nama Anda sendiri yang tidak jelas, tidak mungkin bertabrakan tanpa garis bawah, misalnya mylib_internal___iatau serupa.
R .. GitHub BERHENTI MEMBANTU ICE
2
@R .. Anda benar - Saya sebenarnya sudah membaca ini di '' aplikasi '', kernel Linux, tapi itu pengecualian karena tidak menggunakan pustaka standar (secara teknis, implementasi C '' berdiri bebas '' sebagai gantinya dari yang '' dihosting '').
Blaisorblade
3
@R .. ini tidak sepenuhnya benar: garis bawah utama yang diikuti oleh modal atau garis bawah kedua dicadangkan untuk implementasi dalam semua konteks. Menggarisbawahi terkemuka diikuti oleh sesuatu yang lain tidak dicadangkan dalam lingkup lokal.
Leushenko
@Leushenko: Ya, tetapi perbedaannya cukup halus sehingga saya merasa lebih baik untuk memberi tahu orang-orang tidak menggunakan nama seperti itu sama sekali. Orang-orang yang memahami kehalusan mungkin sudah tahu bahwa aku sedang menyelami detailnya. :-)
R .. GitHub BERHENTI MEMBANTU ICE
7

Penjelasan

do {} while (0)dan if (1) {} elsememastikan bahwa makro diperluas menjadi hanya 1 instruksi. Jika tidak:

if (something)
  FOO(X); 

akan berkembang ke:

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

Dan g(X)akan dieksekusi di luar ifpernyataan kontrol. Ini dihindari saat menggunakan do {} while (0)dan if (1) {} else.


Alternatif yang lebih baik

Dengan ekspresi pernyataan GNU (bukan bagian dari standar C), Anda memiliki cara yang lebih baik daripada do {} while (0)dan if (1) {} elseuntuk menyelesaikannya, hanya dengan menggunakan ({}):

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

Dan sintaks ini kompatibel dengan nilai pengembalian (perhatikan bahwa do {} while (0)tidak), seperti pada:

return FOO("X");
Cur
sumber
3
penggunaan blok-penjepit {} di makro akan cukup untuk bundling kode makro sehingga semua dieksekusi untuk jalur if-condition yang sama. do-while around digunakan untuk menegakkan tanda titik koma di tempat-tempat makro digunakan. dengan demikian makro ditegakkan berperilaku lebih mirip. ini termasuk persyaratan untuk titik koma saat digunakan.
Alexander Stohr
2

Saya tidak berpikir itu disebutkan jadi pertimbangkan ini

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

akan diterjemahkan ke dalam

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

perhatikan bagaimana i++dievaluasi dua kali oleh makro. Ini dapat menyebabkan beberapa kesalahan menarik.

John Nilsson
sumber
15
Ini tidak ada hubungannya dengan do ... while (0) construct.
Trent
2
Benar. Tetapi relevan dengan topik makro vs fungsi dan cara menulis makro yang berperilaku sebagai ...
John Nilsson
2
Mirip dengan yang di atas, ini bukan jawaban tetapi komentar. Pada topik: itu sebabnya Anda menggunakan barang hanya sekali:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos