Брудні трюки з макросами C++

У цій статті я хочу зробити дві речі: розповісти, чому макроси — зло і як з цим боротися, а так само продемонструвати пару використовуваних мною макросів C++, які спрощують роботу з кодом і покращують його читабельність. Трюки, насправді, не такі вже й брудні:
  • Безпечний виклик методу
  • Невикористовувані змінні
  • Перетворення в рядок
  • Кома у рядку макросу
  • Нескінченний цикл
Заздалегідь попереджаю: якщо Ви думаєте побачити під катом щось круте, головоломное і карколомне, то нічого такого в статті немає. Стаття про світлу сторону макросів.

Кілька корисних посилань
Для початківців: стаття (англійською) Anders Lindgren — Tips and tricks using the preprocessor (part one), покриває самі основи макросів.
Для просунутих: стаття (англійською) Anders Lindgren — Tips and tricks using the preprocessor (part two), покриває більш серйозні теми. Дещо буде і в цій статті, але не всі, і з меншою кількістю пояснень.
Для професіоналів: стаття (англійською) Aditya Kumar, Andrew Sutton, Bjarne Stroustrup — Rejuvenating C++ Programs through Demacrofication, описує можливості по заміні макросів на фічі C++11.

Невелику культурну відмінність
Згідно Вікіпедії і моїм власним відчуттям, російською мовою ми зазвичай розуміємо під словом «макрос» ось це:
#define FUNC(x, y) ((x)^(y))
А наступне:
#define VALUE 1
у нас називається «константою препроцесора» (або просто «дефайн»'ом). В англійській мові трохи не так: перше називається function-like macro, а друге — object-like macro (знову ж, наведу посилання на Вікіпедія). Тобто, коли вони говорять про макроси, вони можуть мати на увазі як одне, так і інше, так і всі разом. Будьте уважні при читанні англійських текстів.

Що таке добре і що таке погано
Останнім часом популярна думка, що макроси — зло. Думка це не безпідставно, але, на мій погляд, потребує пояснень. В одному з відповідей на питання Why are preprocessor macros evil and what are the alternatives? я знайшов досить повний список причин, які змушують нас вважати макроси злом і деякі способи від них позбутися. Нижче я наведу цей же список російською, але приклади і вирішення проблем будуть не зовсім такими, як по вказаному посиланню.
  1. Макроси можна налагоджуватиПо-перше, насправді, можна:
    Go to either project or source file properties by right-clicking and going to «Properties». Under Configuration Properties->C/C++->Preprocessor, set «Generate Preprocessed File» to either with or without line numbers, whichever you prefer. This will show what your macro expands to in context. If you need to debug it on live compiled code, just cut and paste that, and put it in place of your macro while debugging.
    Так що, правильніше буде сказати, що «макроси складно налагоджувати». Але, тим не менш, проблема з налагодженням макросів існує.

    Щоб визначити, чи потребує використовуваний Вами макрос в налагодженні, подумайте, чи є в ньому те, заради чого варто захотіти запхати туди точку зупину. Це може бути зміна значень, отриманих через параметри оголошення змінних, зміна об'єктів або даних зовні і тому подібне.Вирішення проблеми:
    • повністю позбутися від макросів, замінивши їх на функції (можна inline, якщо це важливо),
    • логіку макросів перенести в функції, а самі макроси зробити відповідальними тільки за передачу даних в ці функції,
    • використовувати лише макроси, які не вимагають налагодження.
  2. При розгортанні макросу можуть з'явитися дивні побічні ефектиЩоб показати, про які побічні ефекти йдеться, зазвичай наводять приклад з арифметичними операціями. Я теж не буду відступати від цієї традиції:
    #include < iostream>
    #define SUM(a, b) a + b
    int main()
    {
    // Що буде в x?
    int x = SUM(2, 2);
    std::cout << x << std::endl;
    x = 3 * SUM(2, 2);
    std::cout << x << std::endl;
    return 0;
    }
    
    У висновку очікуємо 4 і 12, а отримуємо 4 і 8. Справа в тому, що макрос просто підставляє код туди, куди зазначено. І в даному випадку код буде виглядати так:
    int x = 3 * 2 + 2;
    
    Це і є побічний ефект. Щоб все запрацювало, як очікується, потрібно змінити наш макрос:
    #include < iostream>
    #define SUM(a, b) (a + b)
    int main()
    {
    // Що буде в x?
    int x = SUM(2, 2);
    std::cout << x << std::endl;
    x = 3 * SUM(2, 2);
    std::cout << x << std::endl;
    return 0;
    }
    
    Тепер вірно. Але це ще не все. Перейдемо до множення:
    #define MULT(a, b) a * b
    
    Відразу ж запишемо його «правильно», але використовуємо трохи інакше:
    #include < iostream>
    #define MULT(a, b) (a * b)
    int main()
    {
    // Що буде в x?
    int x = MULT(2, 2);
    std::cout << x << std::endl;
    x = MULT(3, 2 + 2);
    std::cout << x << std::endl;
    return 0;
    }
    
    Дежавю: знову отримуємо 4 і 8. В даному випадку розгорнутий макрос буде виглядати як:
    int x = (3 * 2 + 2);
    
    тобто, тепер нам потрібно написати:
    #define MULT(a, b) (a) * (b)
    
    Використовуємо цю версію макросу і вуаля:
    #include < iostream>
    #define MULT(a, b) (a) * (b)
    int main()
    {
    // Що буде в x?
    int x = MULT(2, 2);
    std::cout << x << std::endl;
    x = MULT(3, 2 + 2);
    std::cout << x << std::endl;
    return 0;
    }
    
    Тепер все правильно.

    Якщо абстрагуватися від арифметичних операцій, то в загальному випадку, при написанні макросів нам потрібні
    • дужки навколо всього виразу
    • дужки навколо кожного з параметрів макросу
    тобто, замість
    #define CHOOSE(ifC, chooseA, otherwiseB) ifC ? chooseA : otherwiseB
    
    має бути
    #define CHOOSE(ifC, chooseA, otherwiseB) ((ifC) ? (chooseA) : (otherwiseB))
    

    Ця проблема ускладнюється тим, що далеко не всі типи параметрів можна обернути в дужки (реальний приклад буде далі в статті). З-за цього зробити якісні макроси буває досить складно.Вирішення проблеми:
    • відмовитися від макросів на користь функцій,
    • використовувати макроси з зрозумілим ім'ям, простою реалізацією і грамотно розставленими дужками, щоб програміст, який використовує такий макрос легко зрозумів, як правильно його використовувати.
  3. Макроси не мають простору іменЯкщо оголошений якийсь макрос, він не тільки глобальний, але ще й просто не дасть скористатися чим-небудь з таким же ім'ям (завжди буде підставлена реалізація макросу). Найбільш, мабуть, найвідомішим прикладом є проблема з min і max під Windows.Рішення проблеми — вибирати імена для макросів, які з низькою ймовірністю перетнуться з ніж, наприклад:
    • імена в UPPERCASE, зазвичай вони можуть перетнутися тільки з іншими іменами макросів,
    • назви з префіксом (ім'я Вашого проекту, namespace, ще щось унікальне), пересічення з іншими іменами буде можливо з дуже невеликою ймовірністю, але використовувати такі макроси за межами Вашого проекту людям буде трохи складніше.
  4. Макроси можуть робити щось, про що Ви не підозрюєтенасправді, це проблема вибору імені для макросу. Скажімо, візьмемо той же приклад, який наведено у відповіді за посиланням:
    #define begin() x = 0
    #define end() x = 17
    ... a few thousand lines of stuff here ... 
    void dostuff()
    {
    int x = 7;
    
    begin();
    
    ... more code using x ... 
    
    printf("x=%d\n", x);
    
    end();
    
    }
    
    Тут у наявності невірно обрані імена, які вводять в оману. Якби макроси були названі set0toX() і set17toX() або якось схоже, проблеми вдалося б уникнути.Вирішення проблеми:
    • грамотно іменувати макроси,
    • замінити макроси функції,
    • не використовувати макроси, які неявно щось змінюють.
Після всього перерахованого вище можна дати визначення «хорошим» макросів. Хороші макроси — це макроси, які
  • не вимагають налагодження (всередині просто немає чого ставити крапку останова)
  • не мають побічних ефектів при розгортанні (всі обгорнуте скобочками)
  • не конфліктують з іменами де-небудь (обраний такий вид імен, які з невеликою часткою ймовірності будуть використані ким-небудь ще)
  • нічого не змінюють неявно (ім'я точно відображає, що робить макрос, а вся робота з навколишнім кодом, по можливості, ведеться тільки через параметри і «значення, що повертається»)
Безпечний виклик методу
#define prefix_safeCall(value, object, method) ((object) ? ((object)->method) : (value))
#define prefix_safeCallVoid(object, method) ((object) ? ((void)((object)->method)) : ((void)(0)))

насправді, я використовую ось таку версію
#define prefix_safeCall(defaultValue, objectPointer, methodWithArguments) ((objectPointer) ? ((objectPointer)->methodWithArguments) : (defaultValue))
#define prefix_safeCallVoid(objectPointer, methodWithArguments) ((objectPointer) ? static_cast<void>((objectPointer)->methodWithArguments) : static_cast<void>(0))
Але Хабр — це не IDE, тому настільки довгі рядки виглядають негарно (принаймні, на моєму моніторі), і я скоротив їх до вигляду зручного для читання.
Зверніть увагу на параметр method. Це той самий приклад параметра, який не можна обернути дужками. Це означає, що крім виклику методу параметр можна запхати і що-небудь ще. Тим не менш, випадково це влаштувати досить проблематично, тому я не вважаю ці макроси «поганими». Схожі макроси (дещо інакше реалізовані) я зустрічав в реальному продакшн коді, він використовувався командою програмістів, в тому числі і мною. Жодних проблем із-за нього на моїй пам'яті не виникало

Як ці два макросу використовуються, думаю, зрозуміло. Якщо маємо код:
auto somePointer = ...;
if(somePointer)
somePoiter->callSomeMethod();
з допомогою макросу safeCallVoid він перетворюється на:
auto somePointer = ...;
prefix_safeCallVoid(somePointer, callSomeMethod());
та, аналогічно, для випадку з її обчислене значення:
auto somePointer = ...;
auto x = prefix_safeCall(0, somePointer, callSomeMethod());

— Для чого? В першу чергу, ці макроси дозволяють збільшити читаність коду, зменшити вкладеність. Найбільший позитивний ефект дають у сукупності з невеликими методами (тобто, якщо слідувати принципам рефакторінгу).

Невикористовувані змінні
#define prefix_unused(variable) ((void)variable)

насправді, використовуваний мною варіант теж відрізняється
#define prefix_unused1(variable1) static_cast<void>(variable1)
#define prefix_unused2(variable1, variable2) static_cast<void>(variable1), static_cast<void>(variable2)
#define prefix_unused3(variable1, variable2, variable3) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3)
#define prefix_unused4(variable1, variable2, variable3, variable4) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4)
#define prefix_unused5(variable1, variable2, variable3, variable4, variable5) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4), static_cast<void>(variable5)
Зверніть увагу, що, починаючи з двох параметрів, даний макрос теоретично може мати побічні ефекти. Для більшої надійності можна скористатися класикою:
#define unused2(variable1, variable2) do {static_cast<void>(variable1); static_cast<void>(variable2);} while(false)
Але, в такому вигляді він складніше читаємо, із-за чого я використовую менш «безпечний» варіант.

Подібний макрос є, наприклад, в cocos2d-x, там він називається CC_UNUSED_PARAM. З недоліків: теоретично, він може працювати не на всіх компіляторах. Тим не менш, в cocos2d-x він для всіх платформ визначений абсолютно однаково.

Використання:
int main()
{
int a = 0; // невживана мінлива.
prefix_unused(a);
return 0;
}

— Для чого? Цей макрос дозволяє уникнути попередження про вільної змінної, а читає код він як би говорить: «той, хто писав це — знав, що змінна не використовується, все в порядку».

Перетворення в рядок
#define prefix_stringify(something) std::string(#something)

Так, ось так суворо, відразу в std::string. Плюси і мінуси використання даних класу залишимо за рамками розмови, поговоримо тільки про макрос.

Використовувати його можна так:
std::cout << prefix_stringify("string\n") << std::endl;
Та ще так:
std::cout << prefix_stringify(std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
Та навіть так:
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)
std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
Проте, в останньому прикладі перенесення рядка буде замінений на пробіл. Для реального перенесення потрібно використовувати '\n':
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)\nstd::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
Також, можна використовувати і інші символи, наприклад '\' для конкатенації рядків, '\t' та інші.

— Для чого? Може використовуватися для спрощення виводу налагоджувальної інформації або, наприклад, для створення фабрики об'єктів з текстовими id (в цьому випадку такий макрос може використовуватися при реєстрації класу в фабриці для перетворення імені класу в рядок).

Кома в параметрі макросу
#define prefix_singleArgument(...) __VA_ARGS__

Ідея підглянена тут.

Приклад звідти ж:
#define FOO(type name) type name
FOO(prefix_singleArgument(std::map<int, int>), map_var);

— Для чого? Використовується при необхідності передати в інший макрос аргумент, що містить коми, як один аргумент і неможливості використовувати для цього дужки.

Нескінченний цикл
#define forever() for(;;)

Версія від Джоела Спольски
#define ever (;;)
for ever { 
...
}
P. S. Якщо хто-небудь, перейшовши за посиланням, не догадався прочитати назву питання, то звучить воно приблизно як «яке найгірше реальне зловживання макросами Вам зустрічалося?» ;-)
Використання:
int main()
{
bool keyPressed = false;
forever()
{
...
if(keyPressed)
break;
}
return 0;
}

— Для чого? Коли while(true), while(1), for(;;) та інші стандартні шляхи створення циклу здаються не дуже інформативними, можна використовувати такий макрос. Едиственный плюс який він дає трохи кращу читабельність коду.

Висновок
При правильному використанні макроси зовсім не є чимось поганим. Головне, не зловживати ними і слідувати нехитрим правилам щодо створення «хороших» макросів. І тоді вони стануть Вашими кращими помічниками.

P. S.
А які цікаві макроси використовуєте Ви у своїх проектах? Не соромтеся поділитися в коментарях.
Ви все ще на світлій стороні C++, або вже пішли по темному шляху?

/>
/>


<input type=«radio» id=«vv64355»
class=«radio js-field-data»
name=«variant[]»
value=«64355» />
Я — джедай! Я взагалі не використовую макроси!
<input type=«radio» id=«vv64357»
class=«radio js-field-data»
name=«variant[]»
value=«64357» />
Я на світлій стороні, використовую тільки «хороші» макроси; або «погані», але перевірені.
<input type=«radio» id=«vv64359»
class=«radio js-field-data»
name=«variant[]»
value=«64359» />
Я на темній стороні, і використовую різні макроси.
<input type=«radio» id=«vv64361»
class=«radio js-field-data»
name=«variant[]»
value=«64361» />
Я на темній стороні! А хіба в C++ є щось ще, крім макросів?
<input type=«radio» id=«vv64363»
class=«radio js-field-data»
name=«variant[]»
value=«64363» />
Я — Імператор Палпатін!

Проголосувало 10 осіб. Утрималося-3 людини.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.