Чому макроси препроцесора є злими і які альтернативи?


94

Я завжди про це питав, але ніколи не отримував справді хорошої відповіді; Я думаю, що майже будь-який програміст ще до написання першого "Hello World" стикався з фразою типу "макрос ніколи не можна використовувати", "макрос - це зло" тощо, моє запитання: чому? З новим C ++ 11 чи існує реальна альтернатива через стільки років?

Найпростіша частина стосується таких макросів, як #pragmaпевна платформа та компілятор, і в більшості випадків вони мають такі серйозні недоліки, як #pragma onceсхильність до помилок щонайменше у 2 важливих ситуаціях: однакові назви в різних шляхах, а також деякі мережеві установки та файлові системи.

Але загалом, як щодо макросів та альтернатив їх використанню?


19
#pragmaне є макросом.
FooF

1
директива препроцесора @foof?
user1849534

6
@ user1849534: Так, ось що це ... і про поради щодо макросів не йдеться #pragma.
Ben Voigt

1
Ви можете зробити багато з constexpr, inlineфункціями, і templates, але boost.preprocessorі chaosпоказати , що макроси мають своє місце. Не кажучи вже про макроси конфігурації для компіляторів різниць, платформ тощо
Брендон

1
можливий дублікат макросів С ++?
Aaron McDaid

Відповіді:


165

Макроси подібні до будь-якого іншого інструменту - молоток, який використовується у вбивстві, не є злом, бо це молоток. Це зло в тому, як людина використовує його таким чином. Якщо ви хочете забити цвяхи, молоток є ідеальним інструментом.

Є кілька аспектів макросів, які роблять їх "поганими" (я розгляну їх на кожному пізніше та запропоную альтернативні варіанти):

  1. Ви не можете налагоджувати макроси.
  2. Розширення макросів може призвести до дивних побічних ефектів.
  3. Макроси не мають "простору імен", тому, якщо у вас є макрос, який зіткнеться з іменем, яке використовується в іншому місці, ви отримуєте заміну макросів там, де цього не хотіли, і це зазвичай призводить до дивних повідомлень про помилки.
  4. Макроси можуть впливати на те, чого ви не уявляєте.

Тож давайте трохи розширимось тут:

1) Макроси неможливо налагодити. Коли у вас є макрос, який перекладається на число або рядок, у вихідному коді буде ім’я макросу, а у багатьох налагоджувачів ви не зможете «побачити», на що перекладається макрос. Тож ви насправді не знаєте, що відбувається.

Заміна : Використовуйте enumабоconst T

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

Заміна : Використовуйте функції - вбудовані, якщо це потрібно "швидко" (але будьте обережні, що занадто багато вбудованих файлів - це не добре)

2) Розширення макросів може мати дивні побічні ефекти.

Знаменитий - це #define SQUARE(x) ((x) * (x))і використання x2 = SQUARE(x++). Це призводить доx2 = (x++) * (x++); , що, навіть якби це був дійсний код [1], майже напевно не був би тим, що хотів програміст. Якби це була функція, було б непогано зробити x ++, і x зростав би лише один раз.

Інший приклад - "if else" у макросах, скажімо, ми маємо таке:

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

і потім

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Насправді це стає абсолютно неправильним ...

Заміна : реальні функції.

3) Макроси не мають простору імен

Якщо у нас є макрос:

#define begin() x = 0

і у нас є деякий код на C ++, який використовує begin:

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

Тепер, яке повідомлення про помилку, на вашу думку, ви отримуєте, і де ви шукаєте помилку [припускаючи, що ви зовсім забули - або навіть не знали про неї - початковий макрос, який міститься в якомусь заголовковому файлі, який написав хтось інший? [і ще цікавіше, якби ви включили цей макрос перед включенням - ви б тонули в дивних помилках, які абсолютно не мають сенсу, коли ви дивитесь на сам код.

Заміна : Ну, тут існує не стільки заміна, скільки "правило" - використовуйте лише макроси у верхньому регістрі, і ніколи не використовуйте всі імена у верхньому регістрі для інших речей.

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

}

Тепер, не дивлячись на макрос, ви могли б подумати, що begin - це функція, яка не повинна впливати на x.

Такі речі, і я бачив набагато складніші приклади, ДІЙСНО можуть зіпсувати ваш день!

Заміна : Або не використовуйте макрос для встановлення x, або передайте x як аргумент.

Бувають випадки, коли використання макросів однозначно вигідно. Одним із прикладів є обгортання функції макросами для передачі інформації про файл / рядок:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Тепер ми можемо використовувати my_debug_mallocяк звичайний malloc в коді, але він має додаткові аргументи, тому, коли справа доходить до кінця і ми скануємо те, "які елементи пам'яті ще не звільнені", ми можемо надрукувати там, де було розподіл, так що програміст може відстежити витік.

[1] Невизначена поведінка оновлювати одну змінну більше одного разу "в точці послідовності". Точка послідовності - це не зовсім те саме, що твердження, але для більшості намірів і цілей саме це ми повинні розглядати. Таким чином x++ * x++, оновлення буде оновлюватися xдвічі, що не визначено і, можливо, призведе до різних значень в різних системах, а також до різного значення результату x.


6
Ці if elseпроблеми можуть бути вирішені шляхом обгортання макро всередині тіла do { ... } while(0). Це веде себе , як і слід було очікувати , з ifі forі інших питань , потенційно ризиковані потоку управління. Але так, реальна функція, як правило, є кращим рішенням. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Аарон Макдейд

11
@AaronMcDaid: Так, є кілька обхідних шляхів, які вирішують деякі проблеми, що виникають у цих макросах. Вся суть мого допису полягала не в тому, щоб показати, як добре робити макроси, а в тому, "як легко помилитися в макросах", де є хороша альтернатива. Тим не менш, є речі, які макроси вирішуються дуже легко, і бувають випадки, коли макроси теж потрібні.
Mats Petersson,

1
У пункті 3 помилки вже насправді не є проблемою. Сучасні компілятори, такі як Clang, скажуть щось подібне note: expanded from macro 'begin'і покажуть, де beginце визначено.
kirbyfan64sos

5
Макроси важко перекласти іншими мовами.
Марко ван де Воорт

1
@FrancescoDondi: stackoverflow.com/questions/4176328 / ... . (Зовсім трохи вниз в цій відповіді, він говорить про I ++ * я ++ і такі
Мати Петерсона

21

Вислів "макроси - це зло" зазвичай стосується використання #define, а не #pragma.

Зокрема, вираз стосується цих двох випадків:

  • визначення магічних чисел як макросів

  • використання макросів для заміни виразів

з новим C ++ 11 є реальна альтернатива через стільки років?

Так, для елементів у списку вище (магічні цифри слід визначати за допомогою const / constexpr, а вирази - за допомогою функцій [normal / inline / template / inline template].

Ось деякі проблеми, що виникають при визначенні магічних чисел як макросів та заміни виразів на макроси (замість визначення функцій для оцінки цих виразів):

  • під час визначення макросів для магічних чисел компілятор не зберігає інформації про тип для визначених значень. Це може спричинити попередження про компіляцію (і помилки) і заплутати людей, які налагоджують код.

  • визначаючи макроси замість функцій, програмісти, що використовують цей код, очікують, що вони працюватимуть як функції, а вони цього не роблять.

Розглянемо цей код:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

Ви могли б очікувати, що a та c становитимуть 6 після присвоєння c (як би це було, з використанням std :: max замість макросу). Натомість код виконує:

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

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

Це означає, що якщо ви визначите макрос вище (для max), ви більше не зможете входити #include <algorithm>в будь-який із наведених нижче кодів, якщо ви явно не напишете:

#ifdef max
#undef max
#endif
#include <algorithm>

Наявність макросів замість змінних / функцій також означає, що ви не можете взяти їх адресу:

  • якщо макрос як константа обчислюється магічним числом, ви не можете передати його за адресою

  • для макросу як функції ви не можете використовувати його як предикат, взяти адресу функції або розглядати її як функтор.

Редагувати: Як приклад, правильна альтернатива #define maxвищезазначеному:

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

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

int a = 0;
double b = 1.;
max(a, b);

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

Якщо цей макс визначений як функція шаблону, компілятор вкаже на неоднозначність, і ви повинні сказати або max<int>(a, b)або max<double>(a, b)(і, таким чином, чітко вказати свій намір).


1
Він не повинен бути специфічним для c ++ 11; Ви можете просто використовувати функції для заміни використання макросів-як-вирази та [static] const / constexpr для заміни використання макросів-як-констант.
utnapistim

1
Навіть C99 дозволяє використовувати const int someconstant = 437;, і він може бути використаний майже всіма способами, як макрос. Так само для невеликих функцій. Є кілька речей, де ви можете написати щось як макрос, який не працюватиме в регулярному виразі на мові C (ви можете створити щось, що в середньому має масив будь-якого типу числа, чого C не може зробити, але C ++ має шаблони для того). Хоча C ++ 11 додає ще кілька речей, які "для цього вам не потрібні макроси", це в основному вже вирішено в попередніх C / C ++.
Mats Petersson

Попереднє збільшення під час передачі аргументу є жахливою практикою кодування. І кожен, хто кодує на C / C ++, не повинен вважати функціональний виклик не макросом.
StephenG

Багато реалізацій добровільно вкладають ідентифікатори в дужки, maxі minякщо за ними йде ліва дужка. Але не слід визначати такі макроси ...
LF

14

Загальна проблема полягає в наступному:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Він надрукує 10, а не 5, оскільки препроцесор розширить його таким чином:

printf("25 / (3+2) = %d", 25 / 3 + 2);

Ця версія безпечніша:

#define DIV(a,b) (a) / (b)

2
цікавий приклад, в основному це просто лексеми без семантики
user1849534

Так. Вони розширюються так, як вони надаються макросу. DIVМакрос може бути переписаний з парою () навколо b.
phaazon

2
Ви маєте на увазі #define DIV(a,b), ні #define DIV (a,b), що дуже відрізняється.
Річі

6
#define DIV(a,b) (a) / (b)недостатньо хороший; як загальна практика, завжди додайте крайні дужки, наприклад:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

Макроси цінні особливо для створення загального коду (параметри макросів можуть бути будь-якими), іноді з параметрами.

Більше того, цей код розміщується (тобто вставляється) в точці, де використовується макрос.

OTOH, подібні результати можуть бути отримані за допомогою:

  • перевантажені функції (різні типи параметрів)

  • шаблони, в C ++ (загальні типи параметрів і значення)

  • вбудовані функції (код місця, де вони викликаються, замість того, щоб переходити до одноточкового визначення - однак, це скоріше рекомендація для компілятора).

редагувати: щодо того, чому макрос поганий:

1) відсутність перевірки типу аргументів (вони не мають типу), тому їх можна легко використовувати неправильно 2) іноді розширюються до дуже складного коду, що може бути важко визначити та зрозуміти у попередньо обробленому файлі 3) легко помилитися -prone код у макросах, наприклад:

#define MULTIPLY(a,b) a*b

а потім зателефонувати

MULTIPLY(2+3,4+5)

що розширюється в

2 + 3 * 4 + 5 (а не в: (2 + 3) * (4 + 5)).

Щоб мати останнє, слід визначити:

#define MULTIPLY(a,b) ((a)*(b))

3

Я не думаю, що є щось погане у використанні визначень препроцесора або макросів, як ви їх називаєте.

Вони є (мета) мовною концепцією, яку можна знайти в c / c ++, і як будь-який інший інструмент, вони можуть полегшити вам життя, якщо ви знаєте, що робите. Проблема макросів полягає в тому, що вони обробляються перед вашим кодом c / c ++ і генерують новий код, який може бути несправним і спричиняти помилки компілятора, які є майже очевидними. З іншого боку, вони можуть допомогти вам зберегти ваш код чистим і заощадити багато набору тексту при правильному використанні, тому це зводиться до особистих переваг.


Крім того, як зазначають інші відповіді, погано розроблені визначення препроцесора можуть створювати код із дійсним синтаксисом, але різним семантичним значенням, що означає, що компілятор не буде скаржитися, і ви ввели в своєму коді помилку, яку буде ще важче знайти.
Санді Хрвіч

3

Макроси на C / C ++ можуть служити важливим інструментом контролю версій. Один і той же код може бути доставлений двом клієнтам із незначною конфігурацією макросів. Я використовую такі речі, як

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

Цей тип функціональних можливостей не так просто можливий без макросів. Макроси насправді є чудовим інструментом управління конфігурацією програмного забезпечення, а не просто способом створення ярликів для повторного використання коду. Визначення функцій з метою повторного використання в макросах однозначно може створити проблеми.


Встановити значення макросів на cmdline під час компіляції, щоб побудувати два варіанти з однієї кодової бази, дуже приємно. на модерації.
kevinf

1
З деякої точки зору, це використання є найнебезпечнішим: інструментам (IDE, статичні аналізатори, рефакторинг) буде важко з’ясувати можливі шляхи коду.
Еренон,

1

Я думаю, що проблема в тому, що макроси недостатньо оптимізовані компілятором, і їх "негарно" читати та налагоджувати.

Часто хорошими альтернативами є загальні функції та / або вбудовані функції.


2
Що змушує вас думати, що макроси недостатньо оптимізовані? Вони являють собою просту заміну тексту, і результат оптимізований так само, як і код, написаний без макросів.
Ben Voigt

@BenVoigt , але вони не враховують семантику , і це може привести до чого - то , що може бути з урахуванням , як "не-оптимального» ... по крайней мере , це мій перший вчив про те , що stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534: Це не означає слово "оптимізовано" в контексті компіляції.
Ben Voigt

1
@BenVoigt Точно, макроси - це просто заміна тексту. Компілятор просто продублює код, це не проблема продуктивності, але може збільшити розмір програми. Особливо актуально в деяких контекстах, де у вас є обмеження щодо розміру програми. Деякі коди настільки заповнені макросами, що розмір програми подвоюється.
Давіде Ікарді,

1

Препроцесорні макроси не є злом, коли їх використовують за призначенням, наприклад:

  • Створення різних версій одного програмного забезпечення за допомогою конструкцій типу #ifdef, наприклад, випуск вікон для різних регіонів.
  • Для визначення значень, пов'язаних з тестуванням коду.

Альтернативи - для подібних цілей можна використовувати певні конфігураційні файли у форматі ini, xml, json. Але їх використання матиме ефект часу роботи на код, якого макрос препроцесора може уникнути.


1
оскільки C ++ 17 constexpr if + файл заголовка, який містить "config" змінні constexpr, може замінити # ifdef.
Enhex
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.