Чому в макросах використовувати зовнішньо безглузді висловлювання до-час та інше?


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типу в кінці ... like ((void) 0) .
Phil1970

1
Нагадуйте, що do whileконструкція не сумісна з операторами return, тому в 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();

що синтаксично неправильно, оскільки інше більше не пов'язане з if. Це не допомагає обертати речі фігурними дужками в макросі, оскільки крапка з комою після дужок синтаксично неправильна.

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є можливість вирішити недоліки препроцесора С. Коли ці посібники зі стилю C говорять про те, щоб звільнити препроцесор C, це те, про що вони турбуються.


18
Це не є вагомим аргументом завжди використовувати дужки, якщо, під час та для висловлювань? Якщо ви завжди хочете робити це (як це потрібно для MISRA-C, наприклад), проблема, описана вище, відпадає.
Стів Мельников,

17
Приклад комами повинен бути, #define BAR(X) (f(X), g(X))інакше пріоритет оператора може зіпсувати семантику.
Стюарт

20
@DawidFerenczy: хоча ми і ти, і я чотири з половиною роки тому добре робимо, ми повинні жити в реальному світі. Якщо ми не можемо гарантувати, що всі ifтвердження тощо, у нашому коді використовують дужки, то загортання макросів подібним чином є простим способом уникнення проблем.
Стів Мельников

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)оскільки перерва впливає на цикл макросу while, а не на цикл for на сайті виклику макросу.
Кріс Клайн

5
@ace void(0)я мав на увазі помилку (void)0. І я вважаю , що це дійсно вирішити «обірваних» ще проблема: повідомлення немає ніякого точка з коми після (void)0. Висяче інше в цьому випадку (наприклад if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) викликає помилку компіляції. Ось живий приклад, який це демонструє
Кріс Клайн

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

Do / while може створити область, тим самим інкапсулюючи код макросу, і в кінці кінців потрібна напівкрапка, тим самим розширившись на потрібний код.

Бонус?

Компілятор C ++ оптимізує цикл do / while, оскільки факт його умови є хибним відомий під час компіляції. Це означає, що макрос такий:

#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", FUNCTION , 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). Існує багато хорошого, але є кілька неосмислених я і незакреслених.
Джонатан Леффлер

@Gnubie: Я думаю, ви все ще знаходитесь тут, і до цього часу ви цього не зрозуміли: ви можете уникати зірочок і підкреслень у коментарях із накидами, тому якщо ви введете \_\_LINE\_\_це, відображатиметься як __LINE__. IMHO, краще просто використовувати форматування коду для коду; наприклад, __LINE__(що не потребує спеціальних обробках). PS Я не знаю, чи це було правдою у 2012 році; з тих пір вони зробили досить багато вдосконалень.
Скотт

1
Зважаючи на те, що мій коментар запізнюється на шість років, але більшість компіляторів C фактично не вбудовують inlineфункції (як це дозволяє стандарт)
Ендрю

53

@ jfm3 - У вас є гарна відповідь на питання. Ви також можете додати, що ідіома макросу також запобігає можливі більш небезпечні (оскільки немає помилки) ненавмисну ​​поведінку з простими висловлюваннями "якщо":

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

if (test) FOO( baz);

розширюється до:

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

що є синтаксично правильним, тому помилки компілятора немає, але, мабуть, ненавмисним наслідком буде те, що g () завжди буде викликатися.


22
"напевно, ненавмисне"? Я б сказав "безумовно ненавмисно", інакше програміста потрібно вивезти і застрелити (на відміну від крутого покарання батогом).
Лоуренс Дол

4
Або їм можуть бути підвищені гроші, якщо вони працюють в трибуквеному агентстві і неправдиво вставляють цей код у широко використовувану програму з відкритим кодом ... :-)
R .. GitHub СТОП ДОПОМОГА ДЖЕРЕЛ

2
І цей коментар просто нагадує мені про рядок виходу з ладу в нещодавній помилці верифікації SSL, знайденій в операційних системах Apple
Джерард Секстон

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і все.
Яків Галка

1
Документування макросу для уникнення непорозуміння повинно бути достатньо. Я погоджуюся, що do ... while (0)краще, але він має один недолік: A breakабо continueкеруватиме do ... while (0)циклом, а не циклом, що містить виклик макросу. Тож ifфокус все ще має значення.
Річард Хансен

2
Я не бачу, куди ви могли б поставити breakабо, continueщо було б видно як у вашій do {...} while(0)псевдо-петлі макросів . Навіть у макропараметрі це призведе до синтаксичної помилки.
Патрік Шлютер

5
Ще одна причина використання do { ... } while(0)замість if whateverконструкції - це ідіоматичний характер. do {...} while(0)Конструкція широко, добре відома і використовується багато багатьма програмістами. Його обґрунтування та документація легко відомі. Не так для ifконструкції. Тому потрібно менше зусиль для того, щоб дбати про перегляд коду.
Патрік Шлютер

1
@tristopia: Я бачив, як люди пишуть макроси, які беруть блоки коду як аргументи (що я не обов'язково рекомендую). Наприклад: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Він може бути використаний як CHECK(system("foo"), break;);, де break;покликаний посилатися на цикл, що CHECK()додає виклик.
Річард Хансен

16

Хоча очікується, що компілятори оптимізують do { ... } while(false);цикли, але є ще одне рішення, яке не потребує цієї конструкції. Рішення полягає у використанні оператора комами:

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

або навіть більш екзотично:

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

Хоча це буде добре працювати з окремими інструкціями, воно не працюватиме з випадками, коли змінні будуються та використовуються як частина #define:

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

З цим вимушений буде використовувати конструкцію do / while.


дякую, оскільки оператор з комами не гарантує порядок виконання, це вкладення є способом нав'язати це.
Маріус

15
@Marius: Неправдиво; оператор коми є точкою послідовності і , таким чином , робить гарантію порядок виконання. Я підозрюю, що ви переплутали його із комою у списках аргументів функцій.
R .. GitHub ЗАСТАНОВИТИ ДІЙ

2
Друга екзотична пропозиція зробила мій день.
Spidey

Просто хотілося додати, що компілятори змушені зберігати поведінку, що спостерігається у програмі, тому оптимізація виконання "/ поки немає" не є великою справою (якщо припустити, що оптимізація компілятора є правильною).
Марко А.

@MarcoA. поки ви правильні, я раніше виявив, що оптимізація компілятора, точно зберігаючи функцію коду, але, змінюючи навколо рядків, які, здавалося б, нічого не роблять у контексті сингулярності, порушать багатопотокові алгоритми. Справа в точці Peterson's Algorithm.
Маріус

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 також є бібліотека препроцесорів. Що в цьому вражає розум?
Рене

Ризик else ((void)0)полягає в тому, що хтось може писати, YOUR_MACRO(), f();і це буде синтаксично дійсним, але ніколи не дзвонить f(). З do whileйого синтаксичною помилкою.
Мельпомена

@melpomene, а як же else do; while (0)?
Карл Лей

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 ... while циклі макросу.

Таким чином, ніколи не використовуйте загальні імена змінних у макросі!


Звичайний шаблон - додавати підкреслення в імена змінних у макросах, наприклад int __i;.
Blaisorblade

8
@Blaisorblade: насправді це неправильно та незаконно C; провідні підкреслення зарезервовані для використання в реалізації. Причина, по якій ви бачили цей "звичайний зразок", полягає в тому, що читають заголовки системи ("реалізація"), які повинні обмежуватися цим зарезервованим простором імен. Для додатків / бібліотек слід вибрати власні неясні, неправдоподібні імена без підкреслень, наприклад, mylib_internal___iтощо.
R .. GitHub ЗАСТАНОВИТИ ДІЯ

2
@R .. Ви маєте рацію - я насправді читав це в «додатку», ядрі Linux, але все одно це виняток, оскільки він не використовує стандартної бібліотеки (технічно це «незалежна» реалізація на C "хостинг").
Blaisorblade

3
@R .. це не зовсім правильно: провідні підкреслення, за якими йде велика або друга підкреслення , зарезервовані для реалізації у всіх контекстах. Провідні підкреслення, за якими слідує щось інше, не зберігаються в локальному масштабі.
Левшенко

@ Левшенко: Так, але відмінність є досить тонкою, що я вважаю, що найкраще сказати людям, щоб взагалі не вживати таких імен. Люди, які розуміють тонкість, імовірно, вже знають, що я заглиблюю деталі. :-)
R .. GitHub ЗАСТАНОВИТИ ДІЙ

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.


Краща альтернатива

З ГНУ виразом заяви (не частина стандарту C), у вас є кращий спосіб , ніж do {} while (0)та if (1) {} elseвирішити цю проблему, просто використовуючи ({}):

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

І цей синтаксис сумісний із значеннями повернення (зверніть увагу, що do {} while (0)це не так), як у:

return FOO("X");

3
використання блочного затискання {} у макросі було б достатнім для зв'язання макрокоду, щоб усі виконувались для того ж шляху if-condition. "час-час" навколо використовується для застосування крапки з комою в місцях, коли макрос використовується. таким чином, макрос примусово поводиться більше, ніж функція. сюди входить вимога щодо крапки з комою при використанні.
Олександр Стор

2

Я не думаю, що це було згадано, тому вважайте це

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

буде перекладено на

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

помітити, як i++оцінюється макрос двічі. Це може призвести до деяких цікавих помилок.


15
Це не має нічого спільного з конструкцією do ... while (0).
Трент

2
Правда. Але відносяться до теми макросів та функцій та як написати макрос, який поводиться як функція ...
Джон Нільссон,

2
Так само, як і вище, це не відповідь, а коментар. По темі: саме тому ви використовуєте речі лише один раз:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.