Макрос проти функції в С


101

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

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


21
Поверніть питання на голову. У якій ситуації кращий макрос? Використовуйте реальну функцію, якщо ви не зможете продемонструвати, що макрос краще.
Девід Геффернан

Відповіді:


113

Макроси схильні до помилок, оскільки вони покладаються на підстановку тексту та не виконують перевірку типу. Наприклад, цей макрос:

#define square(a) a * a

добре працює при використанні з цілим числом:

square(5) --> 5 * 5 --> 25

але робить дуже дивні речі при використанні з виразами:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

Введення круглих дужок навколо аргументів допомагає, але не повністю усуває ці проблеми.

Коли макроси містять декілька висловлювань, ви можете потрапити в проблеми з конструктами управління потоком:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

Звичайною стратегією виправлення цього є встановлення висловлювань всередині циклу "do {...} while (0)".

Якщо у вас є дві структури, які містять поле з однаковим іменем, але відрізняються семантикою, той самий макрос може працювати на обох, із дивними результатами:

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

Нарешті, макроси можуть бути важкими для налагодження, створюючи дивні помилки синтаксису або помилки виконання, які вам потрібно розгорнути, щоб зрозуміти (наприклад, з gcc -E), оскільки налагоджувачі не можуть переступити через макроси, як у цьому прикладі:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

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


43
Що з рекламою на C ++?
Pacerier

4
Погодьтеся, це питання С, не потрібно додавати упередженості.
ideaman42

16
C ++ - це розширення C, яке додає (крім усього іншого) функції, призначені для вирішення цього специфічного обмеження C. Я не прихильник C ++, але я думаю, що це тут тема.
D Coetzee

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

1
Згідно з пунктом 6.5.5.1 ISO / IEC 9899: 1999, "між попередньою та наступною точкою послідовності об'єкт повинен змінити його збережене значення якнайбільше відразу шляхом оцінки виразу". (Подібні формулювання існують у попередніх та наступних стандартах С.). Тому x++*x++не можна сказати, що цей вираз збільшується xдвічі; він фактично посилається на невизначене поведінку , тобто компілятор вільний робити все, що завгодно - він може збільшуватися xдвічі, або один раз, або зовсім не; це може перервати помилку або навіть змусити демонів вилетіти з вашого носа .
Психонавт

39

Особливості макросу :

  • Макрос попередньо обробляється
  • Не перевіряється тип
  • Довжина коду збільшується
  • Використання макросу може призвести до побічних ефектів
  • Швидкість виконання швидше
  • Перед компіляцією ім'я макросу замінюється значенням макросу
  • Корисно там, коли невеликий код з’являється багато разів
  • Макрос не перевіряє помилки компіляції

Особливості функції :

  • Функція компільована
  • Перевірка типу виконана
  • Довжина коду залишається однаковою
  • Ніяких побічних ефектів немає
  • Швидкість виконання - повільніше
  • Під час виклику функції відбувається передача управління
  • Корисно там, коли великий код з’являється багато разів
  • Функція перевіряє помилки компіляції

2
Потрібна посилання "швидкість виконання швидше". Будь-який, навіть дещо грамотний компілятор останнього десятиліття, вбудує функції просто чудово, якщо думає, що це забезпечить користь від продуктивності.
Во

1
Чи не це, в умовах обчислень MCU низького рівня (AVR, тобто ATMega32), макроси є кращим вибором, оскільки вони не збільшують стек викликів, як це роблять виклики функцій?
hardyVeles

1
@hardyVeles Не так. Компілятори, навіть для AVR, можуть вбудовувати код дуже розумно. Ось приклад: godbolt.org/z/Ic21iM
Edward

33

Побічні ефекти великі. Ось типовий випадок:

#define min(a, b) (a < b ? a : b)

min(x++, y)

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

(x++ < y ? x++ : y)

xзбільшується вдвічі в одному заяві. (та невизначена поведінка)


Написання багаторядкових макросів також є болем:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

Вони вимагають а \в кінці кожного рядка.


Макроси не можуть "повернути" нічого, якщо ви не зробите це одним виразом:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Не можна це зробити в макросі, якщо ви не використовуєте оператор виразів GCC. (EDIT: Ви можете використовувати оператор комами ... хоча це помітили ... Але це все ще може бути менш читабельним.)


Порядок операцій: (люб’язно надано @ouah)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

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

(x & 0xFF < 42 ? x & 0xFF : 42)

Але &має нижчий пріоритет ніж <. Тож 0xFF < 42оцінюється першим.


5
і не прикладаючи дужки з макро - аргументів у визначенні макросу може призвести до проблем з черговістю: наприклад,min(a & 0xFF, 42)
ouah

Ага так. Я не бачив ваш коментар, коли я оновлював публікацію. Я думаю, я теж згадаю про це.
Містичний

14

Приклад 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

тоді як:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

Приклад 2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

У порівнянні з:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}

13

Якщо ви сумніваєтесь, використовуйте функції (або вбудовані функції).

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

Існують певні виняткові випадки, коли є переваги використання макросів, серед яких:

  • Загальні функції, як зазначено нижче, ви можете мати макрос, який можна використовувати для різних типів вхідних аргументів.
  • Змінне число аргументів може відображати різні функції замість використання C - х va_args.
    наприклад: https://stackoverflow.com/a/24837037/432509 .
  • Вони можуть необов'язково включати в себе місцеву інформацію, наприклад, налагодження рядки:
    ( __FILE__, __LINE__, __func__). перевірити умови до / після публікації, assertпомилки чи навіть статичні твердження, щоб код не компілювався при неправильному використанні (в основному корисний для налагодження збірок).
  • Перевірка вхідних аргументів. Ви можете робити тести на вхідні аргументи, такі як перевірка їх типу, розміру, перевіряти наявність structчленів перед кастингом
    (може бути корисним для поліморфних типів) .
    Або перевірка масиву відповідає деякій умові довжини.
    дивіться: https://stackoverflow.com/a/29926435/432509
  • Хоча зазначалося, що функції перевіряють тип, C також примушує значення (наприклад, ints / floats). У рідкісних випадках це може бути проблематично. Можна записати макроси, які більш вимогливі, ніж функція щодо їх вхідних аргументів. дивіться: https://stackoverflow.com/a/25988779/432509
  • Їх використання як обгортки для функцій, в деяких випадках ви можете уникати повторень, наприклад ... func(FOO, "FOO");, ви можете визначити макрос, який розширює рядок для васfunc_wrapper(FOO);
  • Коли ви хочете маніпулювати змінними в локальній області виклику, передача покажчика на покажчик працює нормально нормально, але в деяких випадках менше використання макросу.
    (присвоєння декільком змінним для операцій на піксель - це приклад, який ви можете віддати перевагу макросу над функцією ... хоча це все ще багато залежить від контексту, оскільки inlineфункції можуть бути опцією) .

Щоправда, деякі з них покладаються на розширення компілятора, які не є стандартними. Це означає, що ви можете мати менш портативний код або мати ifdefїх у, тому вони можуть скористатися лише тоді, коли компілятор підтримує.


Уникнення багаторазових аргументів

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

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

C11 Загальне

Якщо ви хочете мати squareмакрос, який працює з різними типами і має підтримку C11, ви можете це зробити ...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

Висловлення висловлювань

Це розширення компілятора, що підтримується GCC, Clang, EKOPath & Intel C ++ (але не MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

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

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


1
"... підтримується настільки ж широко." Маю надію, що згаданий вами вираз заяви не підтримується cl.exe? (MS's Compiler)
gideon

1
@gideon, правильно відредагована відповідь, хоча для кожної згаданих функцій не впевнений, чи потрібно мати якусь матрицю підтримки компілятора.
ideaman42

12

Перевірка параметрів та коду не повторюється, що може призвести до розмивання коду Синтаксис макросу також може призвести до будь-якої кількості дивних реберних випадків, коли напівколонки або порядок пріоритетності можуть перешкоджати. Ось посилання, яке демонструє деяке макро зло


6

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


Точка розриву - тут дуже важлива угода, дякую, що вказали на це.
Ганс

6

Функції виконують перевірку типу. Це забезпечує додатковий рівень безпеки.


6

Додавання до цієї відповіді ..

Макроси замінюються безпосередньо в програмі препроцесором (оскільки вони в основному є директивами препроцесора). Тому вони неминуче використовують більше місця в пам'яті, ніж відповідна функція. З іншого боку, функція вимагає більше часу для виклику та повернення результатів, і цього накладного покриття можна уникнути, використовуючи макроси.

Також макроси мають деякі спеціальні інструменти, які можуть допомогти при перенесенні програми на різних платформах.

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

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


3

У відповідях вище я не помітив однієї переваги функцій перед макросами, які, на мою думку, є дуже важливими:

Функції можна передавати як аргументи, макроси не можуть.

Конкретний приклад: Ви хочете написати альтернативну версію стандартної функції 'strpbrk', яка буде приймати замість явного списку символів для пошуку в іншій рядку функцію (покажчик на a), яка поверне 0, поки символ не буде виявлено, що проходить деякий тест (визначений користувачем). Однією з причин, можливо, ви хочете це зробити, щоб ви могли використовувати інші стандартні функції бібліотеки: замість надання явного рядка, повного пунктуації, ви можете передати замість 'ispunct' ctype.h і т.д. Якщо 'ispunct' було реалізовано лише як макрос, це не працює.

Є багато інших прикладів. Наприклад, якщо ваше порівняння виконується макросом, а не функцією, ви не можете передати його до 'qsort' stdlib.h.

Аналогічною ситуацією в Python є "print" у версії 2 проти версії 3 (непрохідний оператор проти прохідної функції).


1
Дякую за цю відповідь
Кірол

1

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

#define MIN(a,b) ((a)<(b) ? (a) : (b))

щось схоже на те

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime буде оцінено 5 разів, що може значно знизити ефективність

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.