Макрос против функции в C

101

Я всегда видел примеры и случаи, когда использование макроса лучше, чем использование функции.

Может ли кто-нибудь объяснить мне на примере недостаток макроса по сравнению с функцией?

Кирол
источник
21
Переверните вопрос с ног на голову. В какой ситуации макрос лучше? Используйте настоящую функцию, если вы не можете продемонстрировать, что макрос лучше.
Дэвид Хеффернан

Ответы:

114

Макросы подвержены ошибкам, потому что они полагаются на текстовую замену и не выполняют проверку типов. Например, этот макрос:

#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 ++ имеет ряд функций, таких как шаблоны, которые помогают создавать сложные полиморфные конструкции безопасным способом без использования макросов; подробности см. в Stroustrup's The C ++ Programming Language .

D Coetzee
источник
43
Что с рекламой C ++?
Pacerier
4
Согласитесь, это вопрос C, не нужно добавлять предвзятость.
ideaman42
16
C ++ - это расширение C, которое добавляет (среди прочего) функции, предназначенные для устранения этого конкретного ограничения C. Я не поклонник C ++, но думаю, что это здесь по теме.
D Coetzee
1
Макросы, встроенные функции и шаблоны часто используются для повышения производительности. Они используются чрезмерно и, как правило, снижают производительность из-за раздувания кода, что снижает эффективность кэша инструкций ЦП. Мы можем создавать быстрые общие структуры данных на C без использования этих методов.
Сэм Уоткинс,
1
Согласно ISO / IEC 9899: 1999 §6.5.1, «Между предыдущей и следующей точкой последовательности объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения». (Подобная формулировка существует в предыдущих и последующих стандартах C.) Таким образом, x++*x++нельзя сказать, что выражение увеличивается xдважды; на самом деле он вызывает неопределенное поведение , что означает, что компилятор может делать все, что захочет - он может увеличиваться xдважды, один раз или не делать вообще; он может прерваться с ошибкой или даже заставить демонов вылететь из вашего носа .
Psychonaut
40

Особенности макроса :

  • Макрос предварительно обработан
  • Нет проверки типа
  • Длина кода увеличивается
  • Использование макроса может вызвать побочный эффект
  • Скорость исполнения является Faster
  • Перед компиляцией имя макроса заменяется значением макроса
  • Полезно, когда небольшой код появляется много раз
  • Макро это не Проверка ошибок компиляции

Особенности функции :

  • Функция скомпилирована
  • Проверка типа завершена
  • Длина кода остается прежней
  • Без побочного эффекта
  • Скорость исполнения является Медленнее
  • Во время вызова функции происходит передача управления
  • Полезно, когда большой код появляется много раз
  • Функциональные проверки ошибок компиляции
Zangw
источник
2
Требуется ссылка "скорость выполнения выше". Любой, даже в некоторой степени компетентный компилятор последнего десятилетия будет просто встраивать функции, если он думает, что это обеспечит повышение производительности.
Voo
1
Разве не в контексте низкоуровневых вычислений MCU (AVR, т.е. ATMega32) макросы - лучший выбор, поскольку они не увеличивают стек вызовов, как это делают вызовы функций?
hardyVeles
1
@hardyVeles Не совсем так. Компиляторы, даже для AVR, могут очень грамотно встраивать код. Вот пример: godbolt.org/z/Ic21iM
Эдвард,
34

Побочные эффекты очень велики. Вот типичный случай:

#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. (РЕДАКТИРОВАТЬ: вы можете использовать оператор запятой, хотя ... не заметили этого ... Но он все равно может быть менее читабельным.)


Порядок действий: (любезно предоставлено @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 01
О да. Не видел вашего комментария, пока обновлял сообщение. Думаю, я тоже упомяну об этом.
Mysticial 01
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 / float). В редких случаях это может вызвать проблемы. Можно написать макросы, которые более требовательны, чем функция к своим входным аргументам. см .: https://stackoverflow.com/a/25988779/432509
  • Их использование в качестве оберток для функций, в некоторых случаях вы можете захотеть избежать повторения, например ... func(FOO, "FOO");, вы можете определить макрос, который расширяет строку для васfunc_wrapper(FOO);
  • Когда вы хотите манипулировать переменными в локальной области видимости вызывающего объекта, передача указателя на указатель работает нормально, но в некоторых случаях использование макроса по-прежнему не вызывает затруднений.
    (присвоение нескольким переменным для попиксельных операций - это пример того, что вы можете предпочесть макрос функции ... хотя это все еще сильно зависит от контекста, поскольку inlineфункции могут быть опцией) .

По общему признанию, некоторые из них полагаются на расширения компилятора, которые не являются стандартными C. Это означает, что вы можете получить менее переносимый код или иметь ifdefих, поэтому они используются только тогда, когда компилятор поддерживает.


Избегайте создания экземпляров с несколькими аргументами

Отмечая это, поскольку это одна из наиболее распространенных причин ошибок в макросах ( x++например, передача , когда макрос может увеличиваться несколько раз) .

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

C11 Generic

Если вам нравится иметь 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функцию для многих разных типов.

idesman42
источник
1
"... поддерживается так же широко ..." Держу пари, что выражение, которое вы упомянули, не поддерживается cl.exe? (Компилятор MS)
gideon
1
@gideon, правильно отредактированный ответ, хотя для каждой упомянутой функции не уверен, что необходимо иметь некоторую матрицу поддержки функций компилятора.
ideasman42
12

Проверка типов параметров и кода не выполняется, что может привести к раздуванию кода. Макросинтаксис также может привести к любому количеству странных крайних случаев, когда точки с запятой или порядок приоритета могут мешать. Вот ссылка, демонстрирующая некое макро- зло

Майкл Дорган
источник
6

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

Джим Макнамара
источник
Точка останова здесь очень важна, спасибо, что указали на нее.
Hans
6

Функции выполняют проверку типов. Это дает вам дополнительный уровень безопасности.

ncmathsadist
источник
6

Добавляем к этому ответу ..

Макросы подставляются непосредственно в программу препроцессором (поскольку они в основном являются директивами препроцессора). Таким образом, они неизбежно используют больше памяти, чем соответствующая функция. С другой стороны, функции требуется больше времени для вызова и возврата результатов, и этих накладных расходов можно избежать, используя макросы.

Также у макросов есть некоторые специальные инструменты, которые могут помочь с переносимостью программы на разные платформы.

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

В целом они являются полезным инструментом в программировании. И как макроинструкции, так и функции могут использоваться в зависимости от обстоятельств.

Никос
источник
3

В приведенных выше ответах я не заметил одного преимущества функций над макросами, которое, на мой взгляд, очень важно:

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

Конкретный пример: вы хотите написать альтернативную версию стандартной функции strpbrk, которая будет принимать вместо явного списка символов для поиска в другой строке функцию (указатель на), которая будет возвращать 0, пока символ не будет обнаружено, что он проходит некоторый тест (определенный пользователем). Одна из причин, по которой вы можете захотеть это сделать, заключается в том, чтобы вы могли использовать другие стандартные библиотечные функции: вместо предоставления явной строки, полной знаков препинания, вы могли бы вместо этого передать ctype.h 'ispunct' и т. Д. Если 'ispunct' был реализован только как макрос, это не сработает.

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

Аналогичная ситуация в Python - «печать» в версии 2 по сравнению с версией 3 (непроходимый оператор или допустимая функция).

Шон Ростами
источник
1
Спасибо за этот ответ
Кайрол
1

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

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

как это

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

functionThatTakeLongTime будет оцениваться 5 раз, что может значительно снизить производительность

Саффер
источник