ทำไมจึงใช้คำสั่ง do-while และ if-else ที่ไม่มีความหมายในแมโคร


787

ในมาโคร C / C ++ หลายรุ่นฉันเห็นโค้ดของมาโครห่อหุ้มสิ่งที่ดูเหมือนเป็นdo whileลูปที่ไม่มีความหมาย นี่คือตัวอย่าง

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

ฉันไม่เห็นว่าdo whileกำลังทำอะไรอยู่ ทำไมไม่ลองเขียนโดยไม่ได้ล่ะ

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

2
ยกตัวอย่างเช่นที่มีอื่นผมจะเพิ่มการแสดงออกของvoidประเภทที่สิ้นสุด ... เหมือน((void) 0)
Phil1970

1
โปรดทราบว่าการdo whileสร้างไม่เข้ากันได้กับคำสั่งการส่งคืนดังนั้นการif (1) { ... } else ((void)0)สร้างมีการใช้งานที่เข้ากันได้มากกว่าในมาตรฐาน C และใน GNU C คุณจะชอบโครงสร้างที่อธิบายไว้ในคำตอบของฉัน
Cœur

คำตอบ:


829

do ... whileและif ... elseจะมีการทำให้มันเพื่อให้อัฒภาคหลังจากแมโครของคุณเสมอหมายถึงสิ่งเดียวกัน สมมติว่าคุณมีสิ่งที่เหมือนมาโครตัวที่สองของคุณ

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

ตอนนี้ถ้าคุณจะใช้BAR(X);ในการif ... elseแถลงที่ร่างของคำสั่ง if ไม่ได้ถูกห่อด้วยวงเล็บปีกกาคุณจะได้รับความประหลาดใจที่ไม่ดี

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

รหัสข้างต้นจะขยายเป็น

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

ซึ่งไม่ถูกต้องทาง syntactically เป็นอย่างอื่นไม่เกี่ยวข้องกับถ้า มันไม่ช่วยในการห่อสิ่งต่าง ๆ ในวงเล็บปีกกาภายในมาโครเนื่องจากเครื่องหมายอัฒภาคหลังจากวงเล็บปีกกาไม่ถูกต้องทาง syntactically

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

การแก้ไขปัญหามีสองวิธี วิธีแรกคือการใช้เครื่องหมายจุลภาคเพื่อเรียงลำดับคำสั่งภายในแมโครโดยไม่ต้องปล้นความสามารถในการแสดงออก

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

บาร์เวอร์ชั่นด้านบนBARจะขยายโค้ดด้านบนไปเป็นสิ่งต่อไปนี้ซึ่งถูกต้องตามหลักไวยากรณ์

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

วิธีนี้ใช้ไม่ได้หากf(X)คุณไม่มีรหัสที่ซับซ้อนกว่าซึ่งจำเป็นต้องอยู่ในบล็อกของตัวเองให้พูดเช่นประกาศตัวแปรท้องถิ่น ในกรณีส่วนใหญ่การแก้ปัญหาคือการใช้บางสิ่งบางอย่างเช่นdo ... whileทำให้แมโครเป็นคำสั่งเดียวที่ใช้เครื่องหมายอัฒภาคโดยไม่มีความสับสน

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

คุณไม่ต้องใช้do ... whileคุณสามารถปรุงอาหารด้วยif ... elseเช่นกันแม้ว่าเมื่อif ... elseขยายออกไปข้างในif ... elseมันจะนำไปสู่ ​​" ห้อยต่องแต่งอย่างอื่น " ซึ่งอาจทำให้เกิดปัญหาห้อยต่องแต่งที่มีอยู่ได้ยากยิ่งขึ้นในรหัสต่อไปนี้ .

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

ประเด็นก็คือใช้เซมิโคลอนในบริบทที่เซมิโคลอนห้อยอยู่นั้นผิดพลาด แน่นอนมันสามารถ (และอาจจะควร) โต้แย้งในจุดนี้ว่ามันจะดีกว่าที่จะประกาศBARว่าเป็นฟังก์ชั่นที่เกิดขึ้นจริงไม่ใช่มาโคร

โดยสรุปแล้วdo ... whileจะมีการแก้ไขข้อบกพร่องของ preprocessor C เมื่อไกด์สไตล์ C เหล่านั้นบอกให้คุณเลิกจ้างตัวประมวลผลล่วงหน้า C นี่คือสิ่งที่พวกเขากังวล


18
นี่ไม่ใช่เหตุผลที่ดีที่จะใช้วงเล็บปีกกาถ้าในขณะที่และสำหรับข้อความสั่ง? หากคุณพยายามทำสิ่งนี้ (เช่นเป็นสิ่งจำเป็นสำหรับ MISRA-C) ปัญหาที่อธิบายข้างต้นจะหายไป
Steve Melnikoff

17
ตัวอย่างเครื่องหมายจุลภาคควรเป็น#define BAR(X) (f(X), g(X))ตัวดำเนินการที่สำคัญมิฉะนั้นอาจทำให้เกิดความผิดพลาดได้
Stewart

20
@DawidFerenczy: แม้ว่าทั้งคุณและฉันจากสี่ปีครึ่งที่ผ่านมาทำจุดดีเราต้องอยู่ในโลกแห่งความจริง ถ้าเราไม่สามารถรับประกันได้ว่าข้อความทั้งหมดifฯลฯ ในรหัสของเราใช้วงเล็บปีกกาแล้วการห่อมาโครเช่นนี้เป็นวิธีที่ง่ายในการหลีกเลี่ยงปัญหา
Steve Melnikoff

8
หมายเหตุ: if(1) {...} else void(0)แบบฟอร์มนั้นปลอดภัยกว่าdo {...} while(0)สำหรับแมโครที่มีพารามิเตอร์เป็นรหัสที่รวมอยู่ในการขยายแมโครเนื่องจากจะไม่เปลี่ยนพฤติกรรมของคำหลักที่หยุดพักหรือคำหลักที่ดำเนินการต่อ ตัวอย่างเช่น: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }ทำให้เกิดพฤติกรรมที่ไม่คาดคิดเมื่อMYMACROถูกกำหนดให้เป็น#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)เพราะตัวแบ่งส่งผลกระทบต่อแมโครในขณะที่วนซ้ำแทนที่จะเป็นแบบวนซ้ำที่ไซต์เรียกแมโคร
Chris Kline

5
@ace ถูกพิมพ์ผิดผมหมายถึงvoid(0) (void)0และผมเชื่อว่านี่จะแก้ปัญหา "ห้อยอื่น" ปัญหา: (void)0แจ้งให้ทราบล่วงหน้าไม่มีอัฒภาคหลังจากที่ การห้อยต่องแต่งอย่างอื่นในกรณีนั้น (เช่นif (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) ทำให้เกิดข้อผิดพลาดในการรวบรวม นี่คือตัวอย่างการแสดงสด
Chris Kline

153

มาโครเป็นข้อความคัดลอก / วางที่ตัวประมวลผลล่วงหน้าจะใส่ในรหัสของแท้ ผู้เขียนของแมโครหวังว่าการแทนที่จะสร้างรหัสที่ถูกต้อง

มีสาม "เคล็ดลับที่ดี" ที่จะประสบความสำเร็จในการที่:

ช่วยให้แมโครทำงานเหมือนรหัสของแท้

รหัสปกติมักจะสิ้นสุดด้วยเซมิโคลอน หากรหัสการดูของผู้ใช้ไม่จำเป็นต้องใช้ ...

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

นี่หมายความว่าผู้ใช้คาดหวังว่าคอมไพเลอร์จะสร้างข้อผิดพลาดหากเซมิโคลอนไม่อยู่

แต่เหตุผลที่ดีที่แท้จริงคือในบางครั้งผู้เขียนแมโครอาจจำเป็นต้องแทนที่แมโครด้วยฟังก์ชันของแท้ ดังนั้นมาโครควรจะเป็นจริงทำตัวเหมือนหนึ่ง

ดังนั้นเราควรมีมาโครที่ต้องการเซมิโคลอน

สร้างรหัสที่ถูกต้อง

ดังที่แสดงในคำตอบของ jfm3 บางครั้งแมโครมีมากกว่าหนึ่งคำสั่ง และถ้ามีการใช้แมโครภายในคำสั่ง if ถ้านี่จะเป็นปัญหา:

if(bIsOk)
   MY_MACRO(42) ;

มาโครนี้สามารถขยายเป็น:

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

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

gฟังก์ชั่นจะถูกดำเนินการโดยไม่คำนึงถึงความคุ้มค่าของbIsOkฟังก์ชั่นจะถูกดำเนินการโดยไม่คำนึงถึงความคุ้มค่าของ

ซึ่งหมายความว่าเราต้องเพิ่มขอบเขตให้กับแมโคร:

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

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

สร้างรหัสที่ถูกต้อง 2

หากแมโครเป็นสิ่งที่ต้องการ:

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

เราอาจมีปัญหาอื่นในรหัสต่อไปนี้:

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

เพราะมันจะขยายเป็น:

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

แน่นอนว่ารหัสนี้จะไม่ได้รับการคอมไพล์ ดังนั้นอีกครั้งโซลูชันกำลังใช้ขอบเขต:

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

รหัสทำงานอย่างถูกต้องอีกครั้ง

การรวมเอฟเฟ็กต์ของขอบเขตแบบกึ่งโคลอน +

มีสำนวน C / C ++ หนึ่งอันที่สร้างเอฟเฟกต์นี้: ลูป do / while:

do
{
    // code
}
while(false) ;

สิ่งที่ต้องทำ / ในขณะที่สามารถสร้างขอบเขตดังนั้น encapsulating โค้ดของแมโครและต้องการเซมิโคลอนในท้ายที่สุดจึงขยายเป็นโค้ดที่ต้องการ

โบนัสหรือไม่

คอมไพเลอร์ C ++ จะปรับให้เหมาะที่สุดกับ do / while loop เนื่องจากความจริงแล้วโพสต์เงื่อนไขนั้นเป็นเท็จนั้นรู้จักกันในเวลารวบรวม ซึ่งหมายความว่าแมโครเช่น:

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

จะขยายอย่างถูกต้องเป็น

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

และรวบรวมและปรับให้เหมาะสมเป็น

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

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

   // Etc.
}

6
โปรดทราบว่าการเปลี่ยนมาโครเป็นฟังก์ชันอินไลน์จะเปลี่ยนมาโครมาตรฐานที่กำหนดไว้ล่วงหน้าเช่นโค้ดต่อไปนี้แสดงการเปลี่ยนแปลงในFUNCTIONและLINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", ฟังก์ชั่น , LINE ); } int main () {Fmacro (); Finline (); กลับ 0 } (คำที่เป็นตัวหนาควรอยู่ในเครื่องหมายขีดล่างคู่ - ตัวจัดรูปแบบไม่ดี!)
Gnubie

6
มีคำตอบเล็ก ๆ น้อย ๆ แต่ไม่มีประเด็นปัญหาที่ไม่สมบูรณ์ทั้งหมด ตัวอย่างเช่น: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }ไม่ใช่ส่วนขยายที่ถูกต้อง xในการขยายตัวที่ควรจะเป็น 32 MY_MACRO(i+7)ปัญหาที่ซับซ้อนมากขึ้นคือสิ่งที่เป็นการขยายตัวของ MY_MACRO(0x07 << 6)และอีกอย่างก็คือการขยายตัวของ มีหลายอย่างที่ดี แต่มีบางอย่างที่ยังไม่ได้โพสต์ของฉัน
Jonathan Leffler

@Gnubie: ฉันว่าคุณยังอยู่ที่นี่และคุณยังไม่ได้คิดออกตอนนี้: คุณสามารถหลบหนีเครื่องหมายดอกจันและขีดล่างในความคิดเห็นที่มีเครื่องหมายแบ็กสแลชดังนั้นถ้าคุณพิมพ์\_\_LINE\_\_มันแสดงว่า __LINE__ IMHO จะเป็นการดีกว่าที่จะใช้การจัดรูปแบบรหัสสำหรับรหัส ตัวอย่างเช่น__LINE__(ซึ่งไม่ต้องการการจัดการพิเศษ) ป.ล. ฉันไม่รู้ว่านี่เป็นเรื่องจริงในปี 2012 หรือไม่ พวกเขาทำการปรับปรุงเครื่องยนต์ค่อนข้างน้อยตั้งแต่นั้นมา
Scott

1
เห็นคุณค่าที่ความคิดเห็นของฉันคือหกปีปลาย แต่ส่วนใหญ่คอมไพเลอร์ C ไม่จริงแบบอินไลน์inlineฟังก์ชั่น (ตามที่ได้รับอนุญาตตามมาตรฐาน)
แอนดรู

53

@ jfm3 - คุณมีคำตอบที่ดีสำหรับคำถาม คุณอาจต้องการเพิ่มว่าสำนวนแมโครยังป้องกันอันตรายที่อาจเกิดขึ้น (เพราะไม่มีข้อผิดพลาด) พฤติกรรมที่ไม่ได้ตั้งใจด้วยคำสั่ง 'ถ้า' ง่าย ๆ :

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

if (test) FOO( baz);

ขยายเป็น:

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

ซึ่งถูกต้องทางไวยากรณ์ดังนั้นจึงไม่มีข้อผิดพลาดของคอมไพเลอร์ แต่มีผลที่ไม่ได้ตั้งใจอาจจะเรียกว่า g () เสมอ


22
"อาจจะไม่ตั้งใจ"? ฉันจะบอกว่า "ไม่ได้ตั้งใจอย่างแน่นอน" มิฉะนั้นผู้เขียนโปรแกรมจำเป็นต้องถูกนำออกไปและยิง (ตรงข้ามกับการตีสอนโดยมีแส้)
Lawrence Dol

4
หรือพวกเขาอาจได้รับการเพิ่มถ้าพวกเขาทำงานให้กับ บริษัท ตัวแทนสามตัวอักษรและแทรกรหัสนั้นลงในโปรแกรมโอเพ่นซอร์สที่ใช้กันอย่างแพร่หลาย ... :-)
R .. GitHub STOP ช่วย ICE

2
และความคิดเห็นนี้ทำให้ฉันนึกถึงบรรทัดที่ล้มเหลวของ goto ในข้อผิดพลาดการตรวจสอบใบรับรอง SSL ล่าสุดที่พบใน Apple OSes
Gerard Sexton

23

คำตอบข้างต้นอธิบายความหมายของโครงสร้างเหล่านี้ แต่มีความแตกต่างอย่างมีนัยสำคัญระหว่างทั้งสองที่ไม่ได้กล่าวถึง ในความเป็นจริงมีเหตุผลที่ชอบdo ... whileการif ... elseก่อสร้าง

ปัญหาของการif ... elseสร้างคือมันไม่ได้บังคับให้คุณใส่เครื่องหมายอัฒภาค เหมือนในรหัสนี้:

FOO(1)
printf("abc");

แม้ว่าเราจะเหลือเซมิโคลอน (โดยไม่ได้ตั้งใจ) โค้ดจะขยายเป็น

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

และจะคอมไพล์อย่างเงียบ ๆ (แม้ว่าคอมไพเลอร์บางคนอาจออกคำเตือนสำหรับรหัสที่เข้าไม่ถึง) แต่printfคำสั่งจะไม่ถูกดำเนินการ

do ... whileการสร้างไม่ได้มีปัญหาดังกล่าวเนื่องจากโทเค็นที่ถูกต้องเท่านั้นหลังจากที่while(0)เป็นเครื่องหมายอัฒภาค


2
@RichardHansen: ยังไม่ดีเท่านี้เนื่องจากจากการดูที่การเรียกใช้แมโครคุณไม่ทราบว่าจะขยายเป็นคำสั่งหรือเป็นนิพจน์ หากมีคนสันนิษฐานว่าในภายหลังเธออาจจะเขียนFOO(1),x++;ว่าอะไรจะทำให้เราคิดบวก เพียงใช้do ... whileและนั่นคือมัน
Yakov Galka

1
การบันทึกแมโครเพื่อหลีกเลี่ยงความเข้าใจผิดควรจะเพียงพอ ฉันยอมรับว่าdo ... while (0)เหมาะสมกว่า แต่มีข้อเสียเดียว: A breakหรือcontinueจะควบคุมการdo ... while (0)วนรอบไม่ใช่การวนซ้ำที่มีการเรียกใช้แมโคร ดังนั้นifเคล็ดลับยังคงมีค่า
Richard Hansen

2
ฉันไม่เห็นตำแหน่งที่คุณสามารถใส่breakหรือcontinueที่จะถูกมองว่าเป็นในdo {...} while(0)ห่วง pseudo-loop ของคุณ แม้ในพารามิเตอร์แมโครมันจะทำให้เกิดข้อผิดพลาดทางไวยากรณ์
Patrick Schlüter

5
เหตุผลที่จะใช้do { ... } while(0)แทนการif whateverสร้างก็คือธรรมชาติของมัน การdo {...} while(0)สร้างนั้นแพร่หลายเป็นที่รู้จักและใช้กันอย่างแพร่หลายโดยโปรแกรมเมอร์หลายคน เหตุผลและเอกสารมันเป็นที่รู้จักกันอย่างง่ายดาย ไม่ใช่สำหรับการifก่อสร้าง ดังนั้นจึงต้องใช้ความพยายามน้อยในการตรวจสอบโค้ด
Patrick Schlüter

1
@Tristopia: ฉันเคยเห็นคนเขียนมาโครที่ใช้บล็อคโค้ดเป็นอาร์กิวเมนต์ (ซึ่งฉันไม่จำเป็นต้องแนะนำ) ตัวอย่างเช่น#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. มันสามารถใช้เช่นCHECK(system("foo"), break;);ที่break;มีวัตถุประสงค์เพื่ออ้างถึงห่วงล้อมรอบการCHECK()ภาวนา
Richard Hansen

16

ในขณะที่คาดว่าคอมไพเลอร์จะปรับdo { ... } while(false);วงให้เหมาะสม แต่ก็มีวิธีแก้ไขปัญหาอื่นที่ไม่ต้องการการสร้าง วิธีแก้ไขคือใช้ตัวดำเนินการคอมม่า:

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

หรือมากกว่า exotically:

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

แม้ว่าวิธีนี้จะใช้ได้ดีกับคำสั่งแยกกัน แต่จะไม่ทำงานกับกรณีที่ตัวแปรถูกสร้างและใช้เป็นส่วนหนึ่งของ #define :

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

ด้วยสิ่งนี้จะถูกบังคับให้ใช้ทำ / ในขณะที่การสร้าง


ขอบคุณเนื่องจากตัวดำเนินการจุลภาคไม่รับประกันการดำเนินการคำสั่งการซ้อนนี้เป็นวิธีการบังคับใช้
Marius

15
@Marius: เท็จ; ประกอบจุลภาคเป็นจุดลำดับและจึงไม่เพื่อดำเนินการรับประกัน ฉันสงสัยว่าคุณสับสนกับเครื่องหมายจุลภาคในรายการอาร์กิวเมนต์ของฟังก์ชัน
.. GitHub หยุดช่วยน้ำแข็ง

2
ข้อเสนอแนะที่แปลกใหม่ที่สองทำให้วันของฉัน
Spidey

เพียงแค่ต้องการเพิ่มว่าคอมไพเลอร์ถูกบังคับให้รักษาโปรแกรมที่สามารถสังเกตได้ดังนั้นการเพิ่มประสิทธิภาพการทำ / ในขณะที่ไปไม่มากเรื่องใหญ่ (สมมติว่าการเพิ่มประสิทธิภาพคอมไพเลอร์ถูกต้อง)
Marco A.

@MarcoA ในขณะที่คุณถูกต้องฉันได้พบในอดีตที่การเพิ่มประสิทธิภาพของคอมไพเลอร์ในขณะที่รักษาฟังก์ชั่นของรหัส แต่โดยการเปลี่ยนรอบบรรทัดซึ่งดูเหมือนจะไม่ทำอะไรเลยในบริบทเอกพจน์จะทำลายอัลกอริทึมแบบมัลติเธรด Peterson's Algorithmในกรณีที่จุด
Marius

11

ไลบรารีตัวประมวลผลล่วงหน้า P99ของ Jens Gustedt (ใช่ความจริงที่ว่าสิ่งนั้นมีอยู่ในใจของฉันเช่นกัน!) ปรับปรุงการif(1) { ... } elseสร้างด้วยวิธีการเล็ก ๆ แต่มีความสำคัญโดยการกำหนดสิ่งต่อไปนี้:

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

เหตุผลสำหรับเรื่องนี้คือไม่เหมือนกับการdo { ... } while(0)สร้างbreakและcontinueยังคงทำงานอยู่ในบล็อกที่กำหนด แต่การ((void)0)สร้างข้อผิดพลาดทางไวยากรณ์ถ้าอัฒภาคถูกละเว้นหลังจากการเรียกแมโครซึ่งมิฉะนั้นจะข้ามบล็อกถัดไป (ที่จริงแล้วไม่มีปัญหา "ห้อยต่องแต่งอย่างอื่น" ที่นี่เนื่องจากการelseเชื่อมโยงกับที่ใกล้ที่สุดifซึ่งเป็นปัญหาหนึ่งในมาโคร)

หากคุณสนใจในสิ่งต่าง ๆ ที่สามารถทำได้อย่างปลอดภัยมากขึ้นหรือน้อยลงด้วยตัวประมวลผลล่วงหน้า C ให้ตรวจสอบไลบรารีนั้น


ในขณะที่ฉลาดมากนี่เป็นสาเหตุหนึ่งที่จะถูกระดมยิงด้วยคำเตือนของคอมไพเลอร์เกี่ยวกับโอกาสที่อาจจะเป็นอันตรายอื่น ๆ
แบ่ง

1
คุณมักจะใช้มาโครเพื่อสร้างสภาพแวดล้อมที่มีอยู่นั่นคือคุณไม่เคยใช้break(หรือcontinue) ภายในมาโครเพื่อควบคุมการวนรอบที่เริ่มต้น / สิ้นสุดนอกนั่นเป็นเพียงรูปแบบที่ไม่ดีและซ่อนจุดออกที่เป็นไปได้
mirabilos

นอกจากนี้ยังมีไลบรารีตัวประมวลผลล่วงหน้าใน Boost สิ่งที่เหลือเชื่อเกี่ยวกับมันคืออะไร?
Rene

ความเสี่ยงกับการelse ((void)0)เป็นว่าคนที่อาจจะมีการเขียนYOUR_MACRO(), f();และมันจะถูกต้องไวยากรณ์ f()แต่ไม่เคยโทร ด้วยdo whileข้อผิดพลาดทางไวยากรณ์
melpomene

@melpomene แล้วงั้นelse do; while (0)เหรอ?
Carl Lei

8

ด้วยเหตุผลบางอย่างฉันไม่สามารถแสดงความคิดเห็นในคำตอบแรก ...

คุณบางคนแสดงให้เห็นว่ามาโครมีตัวแปรในตัว แต่ไม่มีใครพูดถึงว่าคุณไม่สามารถใช้ชื่อใด ๆ ในแมโครได้! มันจะกัดผู้ใช้บางวัน! ทำไม? เนื่องจากอาร์กิวเมนต์อินพุตถูกแทนที่ลงในเทมเพลตแมโครของคุณ และในตัวอย่างมาโครของคุณคุณใช้ชื่อที่ผันแปรได้ซึ่งใช้กันมากที่สุดคือiฉัน

ตัวอย่างเช่นเมื่อแมโครดังต่อไปนี้

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

ใช้ในฟังก์ชั่นต่อไปนี้

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

แมโครจะไม่ใช้ตัวแปรที่ตั้งใจ i ซึ่งถูกประกาศไว้ที่จุดเริ่มต้นของ some_func แต่เป็นตัวแปรโลคัลที่ถูกประกาศใน do ... ในขณะที่วนรอบของแมโคร

ดังนั้นอย่าใช้ชื่อตัวแปรทั่วไปในมาโคร!


รูปแบบปกติคือการเพิ่มขีดในชื่อตัวแปรในแมโคร - int __i;ตัวอย่างเช่น
Blaisorblade

8
@Blaisorblade: ที่จริงมันไม่ถูกต้องและผิดกฎหมาย C; ขีดเส้นใต้ชั้นนำสงวนไว้สำหรับการใช้งานโดยการใช้งาน เหตุผลที่คุณเห็น "รูปแบบปกติ" นี้มาจากการอ่านส่วนหัวของระบบ ("การใช้งาน") ซึ่งจะต้อง จำกัด ตัวเองไว้ในเนมสเปซที่สงวนไว้นี้ สำหรับแอปพลิเคชัน / ไลบรารีคุณควรเลือกชื่อที่คลุมเครือไม่น่าจะเกิดการชนกันโดยไม่มีขีดล่างเช่นmylib_internal___iหรือที่คล้ายกัน
.. GitHub หยุดช่วยน้ำแข็ง

2
@R .. ถูกต้อง - ฉันได้อ่านจริง ๆ แล้วใน '' แอปพลิเคชัน '', ลินุกซ์เคอร์เนล, แต่มันก็เป็นข้อยกเว้นอยู่ดีเพราะมันไม่ได้ใช้ไลบรารี่มาตรฐาน (ในทางเทคนิคแล้ว ของหนึ่ง '' โฮสต์ '' หนึ่ง)
Blaisorblade

3
@R .. นี้ไม่ถูกต้อง: ขีดล่างชั้นนำตามด้วยทุนหรือขีดล่างที่สองถูกสงวนไว้สำหรับการดำเนินการในบริบททั้งหมด ขีดเส้นใต้นำตามด้วยอย่างอื่นไม่ได้ถูกสงวนไว้ในขอบเขตท้องถิ่น
Leushenko

@Lushushenko: ใช่ แต่ความแตกต่างมีความละเอียดอ่อนพอที่ฉันคิดว่ามันดีที่สุดที่จะบอกคนอื่นว่าอย่าใช้ชื่อเหล่านี้เลย คนที่เข้าใจความละเอียดอ่อนน่าจะรู้อยู่แล้วว่าฉันกำลังคัดสรรรายละเอียด :-)
R .. GitHub หยุดช่วย ICE

7

คำอธิบาย

do {} while (0)และif (1) {} elseเพื่อให้แน่ใจว่าแมโครจะขยายเป็น 1 คำสั่งเท่านั้น มิฉะนั้น:

if (something)
  FOO(X); 

จะขยายเป็น:

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

และg(X)จะถูกดำเนินการนอกifคำสั่งควบคุม นี้จะหลีกเลี่ยงเมื่อใช้และdo {} while (0)if (1) {} else


ทางเลือกที่ดีกว่า

ด้วยนิพจน์คำสั่ง GNU (ไม่ใช่ส่วนหนึ่งของมาตรฐาน C) คุณมีวิธีที่ดีกว่าdo {} while (0)และif (1) {} elseแก้ปัญหานี้โดยใช้({}):

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

และไวยากรณ์นี้เข้ากันได้กับค่าส่งคืน (โปรดทราบว่าdo {} while (0)ไม่ใช่) ดังที่:

return FOO("X");

3
การใช้ block-clamping {} ในแมโครจะเพียงพอสำหรับการรวมรหัสแมโครเพื่อให้สามารถดำเนินการทั้งหมดสำหรับเส้นทางแบบมีเงื่อนไขเดียวกัน do-while around ถูกใช้สำหรับการบังคับเซมิโคลอนในตำแหน่งที่แมโครถูกใช้ ดังนั้นแมโคจึงถูกบังคับให้ทำหน้าที่เหมือนกันมากขึ้น รวมถึงข้อกำหนดสำหรับเซมิโคลอนต่อท้ายเมื่อใช้
Alexander Stohr

2

ฉันไม่คิดว่ามันถูกกล่าวถึงดังนั้นพิจารณาเรื่องนี้

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

จะถูกแปลเป็น

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

สังเกตว่า i++มีการประเมินสองครั้งโดยมาโครอย่างไร สิ่งนี้อาจทำให้เกิดข้อผิดพลาดที่น่าสนใจ


15
สิ่งนี้ไม่เกี่ยวข้องกับสิ่งที่ต้องทำ ... ในขณะที่ (0) การสร้าง
เทรนต์

2
จริง แต่เกี่ยวข้องกับหัวข้อของแมโครเทียบกับฟังก์ชั่นและวิธีการเขียนแมโครที่ทำงานเป็นฟังก์ชั่น ...
John Nilsson

2
เช่นเดียวกับข้างต้นนี่ไม่ใช่คำตอบ แต่เป็นความคิดเห็น ในหัวข้อ: นั่นคือเหตุผลที่คุณใช้สิ่งต่าง ๆ เพียงครั้งเดียว:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.