Подтвердить что ты не робот

Когда использовать макросы типа функции в C

Я читал некоторый код, написанный на C этим вечером, и наверху файл был функционально подобранным макросом HASH:

#define HASH(fp) (((unsigned long)fp)%NHASH)

Это заставило меня задуматься, почему кто-то решил реализовать функции таким образом, используя макрос функции, а не реализацию это как обычная ванильная функция С? Каковы преимущества и недостатки каждой реализации?

Спасибо, куча!

4b9b3361

Ответ 1

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

Это может показаться не очень. Но в вашем примере макрос превращается в 1-2 инструкции машинного языка, в зависимости от вашего процессора:

  • Получить значение fp из памяти и поместить его в регистр
  • Возьмите значение в регистре, выполните вычисление модуля (%) на фиксированное значение и оставьте это в том же регистре

тогда как эквивалент функции будет намного более инструкциями машинного языка, как правило, что-то вроде

  • Вставьте значение fp в стек
  • Вызвать функцию, которая также помещает следующий (возвращаемый) адрес в стек
  • Возможно, создайте фрейм стека внутри функции, в зависимости от архитектуры процессора и соглашения ABI.
  • Получить значение fp из стека и поместить его в регистр
  • Возьмите значение в регистре, выполните вычисление модуля (%) на фиксированное значение и оставьте это в том же регистре
  • Возможно, возьмите значение из регистра и верните его в стек, в зависимости от CPU и ABI
  • Если был создан кадр стека, расслабьтесь
  • Введите адрес возврата из стека и возобновите выполнение инструкций.

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

Лично я предпочитаю использовать С++ inline как более удобочитаемый и менее подверженный ошибкам, но встроенные строки также действительно больше напоминают компилятору, который он не должен принимать. Макросы препроцессора - это кувалда, с которой компилятор не может спорить.

Ответ 2

Одним из важных преимуществ реализации на основе макросов является то, что он не привязан к какому-либо конкретному типу параметров. Функциональный макрос в C действует во многих отношениях как функция шаблона в С++ (шаблоны на С++ родились как "более цивилизованные" макросы, BTW). В этом конкретном случае аргумент макроса не имеет конкретного типа. Это может быть абсолютно все, что можно конвертировать для типа unsigned long. Например, если пользователь так радует (и если они готовы принять последствия, связанные с реализацией), они могут передавать типы указателей в этот макрос.

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

#define ABS(x) ((x) >= 0 ? (x) : -(x))

работает со всеми арифметическими типами, в то время как функциональная реализация должна предоставить немало из них (я подразумеваю стандартные abs, labs, llabs и fabs). (И да, я знаю об традиционно упомянутых опасностях такого макроса.)

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

Ответ 3

С одной стороны, макросы плохи, потому что они выполняются препроцессором, который ничего не понимает в языке и не заменяет текст. Они обычно имеют множество ограничений. Я не вижу одного выше, но обычно макросы - уродливые решения.

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

Ответ 4

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

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

Некоторые проблемы, которые я испытывал при сохранении кода, написанного макросами, - это то, что макросы могут выглядеть как функции, но не отображаться в таблице символов, поэтому может быть очень неприятно пытаться отследить их до их истоков в растягивании коды (где эта вещь определена?!). Написание макросов во ВСЕХ CAPS, очевидно, полезно для будущих читателей.

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

Ответ 5

Ваш пример на самом деле не является функцией вообще,

#define HASH(fp) (((unsigned long)fp)%NHASH)
//  this is a cast ^^^^^^^^^^^^^^^
//  this is your value 'fp'       ^^
//  this is a MOD operation          ^^^^^^

Я бы подумал, что это всего лишь способ написать более читаемый код с кастингом и модным отложением, завернутым в один макрос "HASH(fp)"


Теперь, если вы решите написать функцию для этого, это, вероятно, будет выглядеть следующим образом:

int hashThis(int fp)
{
  return ((fp)%NHASH);
}

Весьма избыточная функция для нее,

  • вводит точку вызова
  • вводит настройку и восстановление стека вызовов

Ответ 6

C Preprocessor можно использовать для создания встроенных функций. В вашем примере появится код, вызывающий функцию HASH, но вместо этого это просто встроенный код.

Преимущества выполнения макрофункций были устранены, когда С++ вводил встроенные функции. Многие старые API, такие как MFC и ATL, все еще используют макрофункции для выполнения препроцессорных трюков, но это просто выводит код из строя и труднее читать.