Макросы в си: Макросы в С и С++ / Хабр

Содержание

Макросы и C++ | Microsoft Docs

  • Чтение занимает 2 мин

В этой статье

C++ предлагает новые возможности, некоторые из которых заменяются препроцессором ANSI C. Эти новые возможности повышают безопасность типов и предсказуемость языка.

  • В C++ объекты, объявленные как, const могут использоваться в константных выражениях. Он позволяет программам объявлять константы со сведениями о типе и значении. Они могут объявлять перечисления, которые могут быть просмотрены символами с помощью отладчика. При использовании директивы препроцессора #define для определения констант они не так точны и не являются строго типизированными. Хранилище для объекта не выделяется

    const , если программа не содержит выражение, которое принимает его адрес.

  • Возможности встраиваемых функций C++ вытесняют макросы типов функций. Преимущества использования встраиваемых функций по сравнению с макросами изложены ниже.

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

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

Дополнительные сведения о встроенных функциях см. в разделе встроенная __inline, _ _forceinline.

Для обеспечения обратной совместимости все средства препроцессора, существовавшие в ANSI C и более ранних спецификациях C++, сохранены для Microsoft C++.

См. также

Предопределенные макросы
Макросы (C/C++)

c — Для чего нужны макросы C?

Макросы позволяют кому-либо изменять поведение программы во время компиляции. Учти это:

  • Константы C позволяют фиксировать поведение программы во время разработки
  • Переменные C позволяют изменять поведение программы во время выполнения
  • Макросы Си позволяют изменять поведение программы во время компиляции

Во время компиляции означает, что неиспользуемый код даже не попадет в двоичный файл и что процесс сборки может изменять значения, если он интегрирован с препроцессором макроса. Пример: make Arch = arm (предполагается определение макроса пересылки как cc -DARCH = arm)

Простые примеры: (Из glibc limit.h определите наибольшее значение long)

#if __WORDSIZE == 64
#define LONG_MAX 9223372036854775807L
#else
#define LONG_MAX 2147483647L
#endif

Проверяет (используя #define __WORDSIZE) во время компиляции, если мы компилируем для 32 или 64 бит. В мультибиблиотечной цепочке инструментов параметры -m32 и -m64 могут автоматически изменять размер битов.

(Запрос версии POSIX)

#define _POSIX_C_SOURCE 200809L

Запросы во время компиляции поддержки POSIX 2008. Стандартная библиотека может поддерживать многие (несовместимые) стандарты, но с этим определением она предоставит правильные прототипы функций (пример: getline (), no gets () и т.д.). Если библиотека не поддерживает стандарт, она может выдавать #error во время компиляции, например, вместо сбоя во время выполнения.

(жестко закодированный путь)

#ifndef LIBRARY_PATH
#define LIBRARY_PATH "/usr/lib"
#endif

Определяет, во время компиляции каталог жесткого кода. Может быть изменено, например, с помощью -DLIBRARY_PATH =/home/user/lib. Если бы это был const char *, как бы вы настроили его во время компиляции?

(pthread.h, сложные определения во время компиляции)

# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }

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

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

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

Теперь, если вы спросите, возможно ли заменить все определения макро констант и вызовы функций соответствующими определениями? Ответ — да, но это не просто устранит необходимость изменения поведения программы во время компиляции. Препроцессор все еще будет необходим.

Вариативный макрос — это… Что такое Вариативный макрос?

Вариативный макрос — возможность препроцессором Си при помощи специального макроса объявлять поддержку различного числа аргументов.

Макрос с переменным числом аргументов был представлен в ревизии ISO/IEC 9899:1999 (C99) стандарта языка программирования Си в 1999. Также такие макросы были введены в ISO/IEC 14882:2011 (C++11) стандарта языка программирования C++ в 2011 году[1].

Синтаксис объявления

Синтаксис объявления схож с синтаксисом вариативной функции: пропуск «…» используется для обозначения того, что нуль или более аргументов могут быть переданы. При расширении макросом каждый вызов специального идентификатора __VA_ARGS__ в списке замещения макроса заменяется переданными аргументами.

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

Поддержка

GNU Compiler Collection, начиная с версии 3.0, C++ Builder 2006 и Visual Studio 2005 [1] поддерживают макросы с переменным числом аргументов при компиляции кода как на языке Си, так и на языке C++. Кроме того, GCC поддерживает вариативные макросы при компиляции кода на языке Objective-C.

Пример

Если требуется printf-подобная функция dprintf(), принимающая имя файла и номер строки, из которой вызывается в качестве аргумента, можно использовать следующий макрос:

void realdprintf (char const *file, int line, char const *fmt, ...); 
#define dprintf(...) realdprintf(__FILE__, __LINE__, __VA_ARGS__)

dprintf() может быть вызвана как:

который дополняется до:

realdprintf(__FILE__, __LINE__, "Hello, world");

или:

dprintf("%d + %d = %d", 2, 2, 5);

который дополняется до:

 
realdprintf(__FILE__, __LINE__, "%d + %d = %d", 2, 2, 5);

Альтернативы

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

#ifdef TRACING
#define TRACE(_p)       printf _p
#else
#define TRACE(_p)
#endif

Если макрос TRACING определен во время компиляции, вызов макроса TRACE будет эквивалентен вызову функции printf:

TRACE(("Выполняется строка %d\n", __LINE__));

Если макрос TRACING не был определен, во время работы программы печать сообщения выполняться не будет. Обратите внимание, что параметры вызова данного макроса должны быть заключены в двойные скобки.

В некоторых других случаях вместо вариативных макросов можно использовать функционал stdargs языков Си/C++ и вызов функции vprintf.

См. также

  • Вариативная функция

Примечания

  1. Working draft changes for C99 preprocessor synchronization — http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1653.htm

Источники

Макросы в С

На чтение 5 мин Просмотров 42 Опубликовано

Всякий раз, когда код C компилируется в операционной системе Linux, он передается такому компилятору, который переводит код C в двоичный код перед завершением компиляции и запуском кода C. Препроцессор макроса существует как другое имя препроцессора C. На всем языке C макрос характеризуется как последовательность операторов кода, заданных как метка и затем отбрасываемых, когда требуется этот набор кода. Эти макросы часто начинаются с выражения «#», и компилятор выполняет объявления, которые начинаются с такого знака. Существует два типа макросов C, которые мы обсудим в сегодняшней статье следующим образом:

Объектные макросы: если структуры данных продавались беспорядочно, объектные макросы будут отброшены.

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

Объект как макрос

Макрос, подобный объекту, представляет собой идентификатор, заменяющий значение. Это распространенный способ описания числовых констант. Итак, откройте свою операционную систему Linux и войдите в нее. На момент публикации статьи мы использовали систему Ubuntu 20.04 Linux. После быстрого входа в систему запустите консольную оболочку, нажав «Ctrl + Alt + T» или используя строку поиска в области «Действия».

Пример 1

Итак, создайте файл типа C «test.c» или как угодно назовите его через редактор GNU Nano Editor. Этот редактор использовался для редактирования таких файлов в операционной системе Linux.

Файл откроется быстро. Включите библиотеку «stdio.h». Код представлен на картинке. Мы использовали объект #define как макрос для переменной «new» со значением «42». Мы не определяли его, поскольку мы определяем обычную переменную с точкой с запятой в конце. Нет необходимости в «;» в макросах. Теперь код содержит «основной» метод, который нужно выполнить. В этом основном методе есть один оператор печати. Оператор печати показывает результат переменной «new» с некоторой строкой в ​​ней. Сохраните обновленный файл и вернитесь в терминал через «Ctrl + S» и «Ctrl + X» соответственно.

Перед запуском файла сначала необходимо скомпилировать код. Поэтому убедитесь, что в вашей системе Linux установлен компилятор языка C. На момент написания этого руководства мы использовали компилятор «gcc». Если вы также хотите установить его, попробуйте выполнить следующий запрос в своей консоли.

$ sudo apt install gcc

После установки компилятора мы скомпилируем код с запросом «gcc». Этот запрос должен содержать имя файла для компиляции, как показано ниже.

Теперь компиляция прошла успешно и ошибок не было; мы запустим наш файл с помощью инструкции «a.out», как указано ниже. Выходные данные представлены на изображении, показывающем значение макропеременной.

Пример 2

Теперь у нас есть следующий пример объектно-подобного макроса. Этот пример будет немного отличаться от предыдущего. Итак, откройте тот же файл, чтобы обновить наши строки кода.

Мы определили переменную val со значением float «5.765» в качестве объектно-подобного макроса в верхней части кода после библиотеки. Внутри основной функции две переменные типа float, «r» и «a», были инициализированы без значения во время инициализации. Переменная «r» будет использоваться как радиус, а переменная «a» будет использоваться как «площадь». Операторы печати покажут пользователю сообщение, чтобы ввести радиус по своему выбору. Строка scanf использовалась для получения ввода от пользователя через терминал. Это значение, введенное пользователем, будет считаться значением с плавающей запятой и будет привязано к переменной «r». Мы вычисляли площадь «а», вычисляя объектно-подобную макропеременную и радиус, введенные пользователем в следующей строке. После этого рассчитанная площадь будет отображаться на экране в виде оператора печати.

Скомпилируйте код документа с помощью «gcc».

Запустите файл и введите радиус по запросу, и он рассчитает площадь для введенного вами значения.

Функция как макрос

В функции, подобной макросу, мы определим функцию вместо любой переменной. Итак, приступим.

Пример 1

Откройте документ C test.c, чтобы изменить код.

Функция «слияние» используется как макрос с двумя переменными в своем параметре. Вы должны определить логику функции при использовании макрос-функции #define, потому что ваша машина не понимает без нее. Итак, мы определили «a ## b». Основной метод покажет слияние двух значений целочисленного типа, переданных в качестве аргумента функции слияния в терминале через оператор печати.

Компиляцию можно выполнить с помощью ключевого слова «gcc».

При выполнении файла «test.c» вы получите объединенное значение обоих двух значений целочисленного типа, которое было передано в операторе печати методу слияния.

Пример 2

Давайте рассмотрим последний пример такой функции, как макрос. На этот раз мы печатаем строковое значение с определенным пределом. Откройте C-файл «test.c».

Мы реализовали функцию MACRO, в которой переменная «a» представляет начальное значение, а «lim» представляет конец ограничения. Пока «a» меньше, чем «lim», он будет печатать строку «Aqsa» и увеличивать переменную «a». Основной метод содержит начальное значение «a», а метод MACRO имеет значение «8», переданное в нем переменной «lim», поэтому он должен напечатать строку «8» раз.

Скомпилировать через:

При выполнении кода строка «Aqsa» печатается 8 раз.

Заключение

В этом руководстве мы рассмотрели как объектные, так и функциональные макросы. Мы надеемся, что он развеет все ваши сомнения и прояснит ваши представления о макросах C.

Почему макросы не включены в большинство современных языков программирования?

Чтобы ответить на ваши вопросы, подумайте, для чего преимущественно используются макросы (Предупреждение: скомпилированный мозг).

  • Макросы, используемые для определения символических констант #define X 100

Это можно легко заменить на: const int X = 100;

  • Макросы, используемые для определения (по существу) встроенных функций, не зависящих от типа #define max(X,Y) (X>Y?X:Y)

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

  • Макросы используются для указания ярлыков для часто используемых элементов. #define p printf

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

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

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

1.19. Определение макроса. C++. Сборник рецептов

Читайте также

Определение

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

4.1.1 Определение

4.1.1 Определение Индексы существуют на диске в статической форме и ядро считывает их в память прежде, чем начать с ними работать. Дисковые индексы включают в себя следующие поля:• Идентификатор владельца файла. Права собственности разделены между индивидуальным

Определение IP по ICQ

Определение IP по ICQ Чтобы определить IP того, кто общается с вами посредством ICQ, достаточно воспользоваться программой UIN2IP (http://neptunix.narod.ru/uin.htm).Вот список некоторых функций UIN2IP:– автоматическое обновление листа;– автоматическое копирование IP-адреса в буфер при двойном

2.2.1. Определение

2.2.1. Определение В общем смысле, контекстная реклама – это вид интернет-рекламы, демонстрируемой человеку в зависимости от содержимого интернет-страницы, на которой она расположена. Например, объявление о продаже автомобиля на сайте об автомобилях, реклама сотовых

Другие опции диалогового окна Запись макроса

Другие опции диалогового окна Запись макроса В зависимости от приложения, диалоговое окно Запись макроса может содержать различные опции. Вот несколько примеров.* Возможно, там будет место для ввода более подробного описания макроса.* Возможно, у вас будут варианты при

Редактирование программного кода макроса в редакторе Visual Basic

Редактирование программного кода макроса в редакторе Visual Basic После того как макрос записан, полученную VBA-программу вы можете отредактировать (или, по крайней мере, просто взглянуть на строки ее программного кода). Вот как это сделать.1. Выберите Сервис=Макрос=Макросы или

Выполнение макроса

Выполнение макроса Запуск макроса из диалогового окна Макрос вряд ли можно назвать большим достижением.Выполняемая при этом последовательность шагов должна быть такой.1. Выберите макрос из списка ниже поля Имя.2. Щелкните на кнопке Выполнить.Ну как, круто? Как

Поиск макроса в диалоговом окне Макрос

Поиск макроса в диалоговом окне Макрос В диалоговом окне Макрос имена приведенных в списке VBA-программ (макросов) могут иногда сбивать с толку. В зависимости от того, где хранится программа, она может приводиться в списке с идентифицирующим ее префиксом.Рассмотрим для

Назначение макроса кнопке панели быстрого запуска

Назначение макроса кнопке панели быстрого запуска Если вам будет удобно вызывать макрос с панели быстрого доступа, то сделайте следующее.1. Нажмите кнопку кнопке в области Назначить макрос. Появится окно Параметры Word с открытым разделом Настройка (рис. 9.3). Рис. 9.3.

Назначение макроса клавишам

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

Запись макроса

Запись макроса Когда подготовительная работа завершена, переходите к записи макроса. После того как вы закроете окна назначения макроса кнопке панели быстрого доступа или клавишам, программа перейдет в режим записи макроса. Указатель при этом примет вид а кнопка в

Выполнение макроса с помощью окна Макрос

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

Определение нотации

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

Использование макроса при выполнении сложного запроса

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

Макросы: Создание собственных макросов

8. Макросы: Создание собственных макросов

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

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

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

История Мака: обычная такая история

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

В отчаянии большой босс нанял младшего (junior) программиста, Мака, чьей работой стал поиск комментариев, написание требуемого кода и вставка его в программу на место комментариев. Мак никогда не запускал программы, ведь они не были завершены и поэтому он попросту не мог этого сделать. Но даже если бы они были завершены, Мак не знал, какие данные необходимо подать на их вход. Поэтому он просто писал свой код, основываясь на содержимом комментариев, и посылал его назад создавшему комментарий программисту.

С помощью Мака все программы вскоре были доделаны, и компания заработала уйму денег продавая их: так много денег, что смогла удвоить количество программистов. Но по какой-то причине никто не думал нанимать кого-то в помощь Маку; вскоре он один помогал нескольким дюжинам программистов. Чтобы не тратить все свое время на поиск комментариев в исходном коде, Мак внес небольшие изменения в используемый программистами компилятор. Теперь, если компилятор встречал комментарий, то отсылал его электронной почтой Маку, а затем ждал ответа с замещающим комментарий кодом. К сожалению, даже с этими изменениями Маку было тяжело удовлетворять запросам программистов. Он работал так тщательно, как только мог, но иногда, особенно когда записи не были ясны, он допускал ошибки.

Однако программисты обнаружили, что чем точнее они пишут свои комментарии, тем больше вероятность того, что Мак вернет правильный код. Как-то раз один из программистов, встретив затруднение с описанием в словах нужного кода, включил в один из комментариев программу на Lisp, которая генерировала нужный код. Такой комментарий был удобен Маку: он просто запустил программу и послал результат компилятору.

Следующее новшество появилось, когда программист вставил в самый верх одной из своих программ комментарий, содержащий определение функции и пояснение, гласившее: «Мак, не пиши здесь никакого кода, но сохрани эту функцию на будущее; я собираюсь использовать ее в некоторых своих комментариях.» Другие комментарии в этой программе гласили следующее: «Мак, замени этот комментарий на результат выполнения той функции с символами x и y как аргументами.»

Этот метод распространился так быстро, что в течение нескольких дней большинство программ стало содержать дюжины комментариев с описанием функций, которые использовались только кодом в других комментариях. Чтобы облегчить Маку различение комментариев, содержащих только определения и не требующих немедленного ответа, программисты отмечали их стандартным предисловием: «Definition for Mac, Read Only» (Определение для Мака, только для чтения). Это (как мы помним, программисты были очень ленивы) быстро сократилось до «DEF. MAC. R/O», а потом до «DEFMACRO».

Очень скоро в комментариях для Мака вообще не осталось английского. Целыми днями он читал и отвечал на электронные письма от компилятора, содержащие DEFMACRO комментарии и вызывал функции, описанные в DEFMACRO. Так как Lisp программы в комментариях осуществляли всю реальную работу, то работа с электронными письмами перестала быть проблемой. У Мака внезапно стало много свободного времени, и он сидел в своем кабинете и грезил о белых песчаных пляжах, чистой голубой океанской воде и напитках с маленькими бумажными зонтиками.

Несколько месяцев спустя программисты осознали что Мака уже довольно давно никто не видел. Придя в его кабинет, они обнаружили, что все покрыто тонким слоем пыли, стол усыпан брошюрами о различных тропических местах, а компьютер выключен. Но компилятор продолжал работать! Как ему это удавалось? Выяснилось, что Мак сделал заключительное изменение в компиляторе: вместо отправки электронного письма с комментарием Маку компилятор теперь сохранял функции, описанные с помощью DEFMACRO комментариев, и запускал при вызове их из других комментариев. Программисты решили, что нет оснований говорить большим боссам, что Мак больше не приходит на работу. Так происходит и по сей день: Мак получает зарплату и время от времени шлет программистам открытки то из одной тропической страны, то из другой.

Время раскрытия макросов против времени выполнения

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

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

(defun foo (x)
  (when (> x 10) (print 'big)))

Обычно вы бы думали о x как о переменной, которая будет содержать аргумент, переданный при вызове foo. Но во время раскрытия макросов (например когда компилятор выполняет макрос WHEN) единственными доступными данными является исходный код. Так как программа пока не выполняется, нет вызова foo и, следовательно, нет значения, ассоциированного с x. Вместо этого, значения, которые компилятор передает в WHEN, являются списками Lisp, представляющими исходный код, а именно (> x 10) и (print 'big). Предположим, что WHEN определен, как вы видели в предыдущей главе, подобным образом:

(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))

При компиляции кода foo, макрос WHEN будет запущен с этими двумя формами в качестве аргументов. Параметр condition будет связан с формой (> x 10), а форма (print 'big) будет собрана (will be collected) в список (и будет его единственным элементом), который станет значением параметра &rest body. Выражение квазицитирования затем сгенерирует следующий код:

(if (> x 10) (progn (print 'big)))

подставляя значение condition, а также вклеивая значение body в PROGN.

Когда Lisp интерпретируется, а не компилируется, разница между временем раскрытия макросов и временем выполнения менее очевидна, так как они «переплетены» во времени (temporally intertwined). Также стандарт языка не специфицирует в точности того, как интерпретатор должен обрабатывать макросы: он может раскрывать все макросы в интерпретируемой форме, а затем интерпретировать полученный код, или же он может начать с непосредственно интерпретирования формы и раскрывать макросы при их встрече. В обоих случаях макросам всегда передаются невычисленные объекты Lisp, представляющие подформы формы макроса, и задачей макроса все также является генерирование кода, который затем осуществит какие-то действия, а не непосредственное осуществление этих действий.

DEFMACRO

Как вы видели в главе 3, макросы на самом деле определяются с помощью форм DEFMACRO, что означает, разумеется, «DEFine MACRO», а не «Definition for Mac». Базовый шаблон DEFMACRO очень похож на шаблон DEFUN.

(defmacro name (parameter*)
"Optional documentation string."
body-form*)

Подобно функциям, макрос состоит из имени, списка параметров, необязательной строки документации и тела, состоящего из выражений Lisp 1). Однако, как я только что говорил, работой макроса не является осуществление какого-то действия напрямую, — его работой является генерирование кода, который затем сделает то, что вам нужно.

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

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

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

После того, как у вас есть пример вызова и его желаемое раскрытие, вы готовы ко второму шагу: фактическому написанию кода макроса. Для простых макросов это будет тривиальным делом написания шаблона-квазицитирования с параметрами макроса, вставленными на нужные места. Сложные макросы сами будут значительными программами, использующими вспомогательные функции и структуры данных.

После того, как вы написали код, преобразующий пример вызова в соответствующее раскрытие, вам нужно убедиться в том, что у абстракции, предоставляемой макросом, нет «протечек» деталей реализации. Предоставляемые макросами «дырявые» абстракции будут работать хорошо только для определенных аргументов или будут взаимодействовать с кодом вызывающего окружения нежелательными способами. Как оказывается, макросы могут «протекать» лишь небольшим количеством способов, все из которых легко избежать, если вы знаете, как выявлять их. Я обсужу как это делается в секции «Устранение протечек».

Подводя итог можно сказать, что шаги по написанию макросов следующие:

1. Написание примера вызова макроса, а затем кода, в который он должен быть раскрыт (или в обратном порядке).

2. Написание кода, генерирующего написанный вручную код раскрытия по аргументам в примере вызова.

3. Проверка того, что предоставляемая макросом абстракция не «протекает».

Пример макроса: do-primes

Для того, чтобы увидеть, как этот трёхшаговый процесс осуществляется, вы напишете макрос do-primes, который предоставляет конструкцию итерирования, подобную DOTIMES и DOLIST, за исключением того, что вместо итерирования по целым числам или элементам списка итерирование будет производиться по последовательным простым числам. Этот пример не является примером чрезвычайно полезного макроса, он — всего лишь средство демонстрации вышеописанного процесса.

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

(defun primep (number)
  (when (> number 1)
    (loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))

(defun next-prime (number)
  (loop for n from number when (primep n) return n))

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

(do-primes (p 0 19)
  (format t "~d " p))

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

Без использования макроса do-primes вы можете написать такой цикл путем использования DO (и двух вспомогательных функций, определенных ранее) следующим образом:

(do ((p (next-prime 0) (next-prime (1+ p))))
    ((> p 19))
  (format t "~d " p))

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

Макропараметры

Так как аргументы, передаваемые в макрос, являются объектами Lisp, представляющими исходный код вызова макроса, первым шагом любого макроса является извлечение тех частей этих объектов, которые нужны для вычисления раскрытия. Для макросов, которые просто подставляют свои аргументы напрямую в шаблон, этот шаг тривиален: подходит простое определение правильных параметров для захвата нужных аргументов.

Но, кажется, такого подхода недостаточно для do-primes. Первый аргумент вызова do-primes является списком, содержащим имя переменной цикла, p; нижнюю границу, 0; верхнюю границу, 19. Но, если вы посмотрите на раскрытие, список, как целое, не встречается в нем: эти три элемента разделены и вставлены в различные места.

Вы можете определить do-primes с двумя параметрами, первый для захвата этого списка и параметр &rest для захвата форм тела цикла, а затем разобрать первый список вручную подобным образом:

(defmacro do-primes (var-and-range &rest body)
  (let ((var (first var-and-range))
        (start (second var-and-range))
        (end (third var-and-range)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

Очень скоро я объясню как тело макроса генерирует правильное раскрытие; сейчас же вам следует отметить, что переменные var, start и end, каждая содержит значение, извлеченное из var-and-range, и эти значения затем подставляются в выражение квазицитирования, генерирующее раскрытие do-primes.

Однако, вам не нужно разбирать var-and-range вручную, так как список параметров макроса является так называемым списком деструктурируемых параметров. Деструктурирование, как и говорит название, осуществляет разбор некоторой структуры, в нашем случае списочной структуры форм, переданных макросу.

Внутри списка деструктурируемых параметров простое имя параметра может быть заменено вложенным списком параметров. Параметры в таком списке будут получать свои значения из элементов выражения, которое было бы связано с параметром, замененным этим списком. Например, вы можете заменить var-and-range списком (var start end) и три элемента списка будут автоматически деструктурированы в эти три параметра.

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

Таким образом, вы можете улучшить определение макроса do-primes и дать подсказку (как людям, читающим ваш код, так и вашим инструментам разработки) об его предназначении:

(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ,end))
     ,@body))

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

(do-primes var-and-range &rest body)

С новым же описанием она сможет указать вам, что вызов должен выглядеть следующим образом:

(do-primes (var start end) &body body)

Списки деструктурируемых параметров могут содержать параметры &optional, &key и &rest, а также вложенные деструктурируемые списки. Однако все эти возможности не нужны вам для написания do-primes.

Генерация раскрытия

Так как do-primes является довольно простым макросом, после деструктурирования аргументов, всё что вам остаётся сделать — это подставить их в шаблон для получения раскрытия.

Для простых макросов, наподобие do-primes, лучшим вариантом является использование специального синтаксиса квазицитирования. Коротко говоря, выражения квазицитирования подобны выражениям цитирования, за исключением того, что вы можете «раскавычить» определенные подвыражения, предваряя их запятой, за которой возможно следует знак «at» (@). Без этого знака «at» запятая вызывает включение как есть значения следующего за ней подвыражения. Со знаком «at» значение, которое должно быть списком, «вклеивается» в окружающий список.

Другой пригодный способ думать о синтаксисе квазицитирования как об очень кратком способе написания кода, генерирующего списки. Такое представление о нем имеет преимущество, так как является очень близким к тому, что на самом деле происходит «под капотом»: когда процедура чтения считывает выражение квазицитирования, она преобразует его в код, который генерирует соответствующую списковую структуру. Например, `(,a b) вероятно будет прочитано как (list a ‘b). Стандарт языка не указывает, какой в точности код процедура чтения должна выдавать, пока она генерирует правильные списковые структуры.

Таблица 8-1 показывает некоторые примеры выражений квазицитирования вместе с эквивалентным создающим списки кодом, а также результаты, которые вы получите при вычислении как выражений квазицитирования, так и эквивалентного кода2):

Таблица 8-1. Примеры квазицитирования
Синтаксис квазицитирования Эквивалентный создающий списки код Результат
`(a (+ 1 2) c) (list ‘a ‘(+ 1 2) ‘c) (a (+ 1 2) c)
`(a ,(+ 1 2) c) (list ‘a (+ 1 2) ‘c) (a 3 c)
`(a (list 1 2) c) (list ‘a ‘(list 1 2) ‘c) (a (list 1 2) c)
`(a ,(list 1 2) c) (list ‘a (list 1 2) ‘c) (a (1 2) c)
`(a ,@(list 1 2) c) (append (list ‘a) (list 1 2) (list ‘c)) (a 1 2 c)

Важно заметить, что нотация квазицитирования является просто удобством. Но это большое удобство. Для оценки того, насколько оно велико, сравните версию do-primes с квазицитированием со следующей версией, которая явно использует создающий списки код:

(defmacro do-primes-a ((var start end) &body body)
  (append '(do)
          (list (list (list var
                            (list 'next-prime start)
                            (list 'next-prime (list '1+ var)))))
          (list (list (list '> var end)))
          body))

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

CL-USER> (do-primes (p 0 19) (format t "~d " p))
2 3 5 7 11 13 17 19
NIL

Или же вы можете проверить макрос напрямую, посмотрев на раскрытие определенного вызова. Функция MACROEXPAND-1 получает любое выражение Lisp в качестве аргумента и возвращает результат осуществления одного шага раскрытия макроса3). Так как MACROEXPAND-1 является функцией, для дословной передачи ей формы макроса вы должны зацитировать эту форму. Теперь вы можете воспользоваться MACROEXPAND-1 для просмотра раскрытия предыдущего вызова4).

CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P 19))
(FORMAT T "~d " P))
T

Также, для большего удобства, в SLIME вы можете проверить раскрытие макроса поместив курсор на открывающую скобку формы макроса в вашем исходном коде и набрав C-c RET для вызова функции Emacs slime-macroexpand-1, которая передаст форму макроса в MACROEXPAND-1 и напечатает результат во временном буфере.

Теперь вы можете видеть, что результат раскрытия макроса совпадает с исходным (написанным вручную) раскрытием, и поэтому кажется, что do-primes работает.

Устранение протечек

В своем эссе «Закон дырявых абстракций» Джоэл Спольски придумал термин «дырявой абстракции» для описания такой абстракции, через которую «протекают» детали, абстрагирование от которых предполагается. Так как написание макроса — это способ создания абстракции, вам следует убедиться, что ваш макрос излишне не «протекает»5)

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

Текущее определение страдает от одной из трех возможных «протечек» макросов, а именно, оно вычисляет подформу end слишком много раз. Предположим, что вы вызвали do-primes с таким выражением, как (random 100), на месте параметра end вместо использования числового литерала, такого, как 19.

(do-primes (p 0 (random 100))
  (format t "~d " p))

Предполагаемым поведением здесь является итерирование по простым числам от нуля до какого-то случайного простого числа, возвращенного (random 100). Однако, это не то, что делает текущая реализация, как это показывает MACROEXPAND-1.

CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
    ((> P (RANDOM 100)))
  (FORMAT T "~d " P))
T

При запуске кода раскрытия RANDOM будет вызываться при каждой проверке условия окончания цикла. Таким образом, вместо итерирования, пока p не станет больше, чем изначально выбранное случайное число, этот цикл будет осуществляться пока не случится, что выбранное в очередной раз случайное число окажется меньше текущего значения p. Хотя общее число итераций по прежнему случайно, оно будет подчиняться вероятностному распределению, отличному от равномерного распределения результатов RANDOM.

Это является «протечкой» абстракции, так как для корректного использования макроса, его пользователь должен быть осведомлен о том, что форма end будет вычислять более одного раза. Одним из способов устранения этой «протечки» является простое специфицирование ее как поведения do-primes. Но это не достаточно удовлетворительно: при реализации макросов вам следует пытаться соблюдать Правило Наименьшего Удивления. К тому же программисты обычно ожидают, что формы, которые они передают макросам, будут вычисляться не большее число раз, чем это действительно необходимо6). Более того, так как do-primes построена на основе модели стандартных макросов DOTIMES и DOLIST, которые вычисляют однократно все свои формы, кроме форм тела, то большинство программистов будут ожидать от do-primes подобного поведения.

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

(defmacro do-primes ((var start end) &body body)
  `(do ((ending-value ,end)
        (,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ending-value))
     ,@body))

К сожалению данное исправление вводит две новые «протечки» в предоставляемую нашим макросом абстракцию.

Одна из этих «протечек» подобна проблеме множественных вычислений, которую мы только что исправили. Так как формы инициализации переменных цикла DO вычисляются в том порядке, в каком переменные определены, то когда раскрытие макроса вычисляется, выражение, переданное как end, будет вычислено перед выражением, переданным как start, то есть в обратном порядке от того, как они идут в вызове макроса. Эта «протечка» не вызывает никаких проблем пока start и end являются литералами вроде 0 и 19. Но, если они являются формами, которые могут иметь побочные эффекты, вычисление их в неправильном порядке снова нарушает Правило Наименьшего Удивления.

Эта «протечка» устраняется тривиально путем изменения порядка определения двух переменных.

(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
        (ending-value ,end))
       ((> ,var ending-value))
     ,@body))

Последняя «протечка», которую нам нужно устранить, была создана использованием имени переменной ending-value. Проблема заключается в том, что имя, которое должно быть полностью внутренней деталью реализации макроса, может вступить во взаимодействие с кодом, переданным макросу, или с контекстом, в котором макрос вызывается. Следующий, кажущийся вполне допустимым, вызов do-primes не работает корректно из-за данной «протечки»:

(do-primes (ending-value 0 10)
  (print ending-value))

То же касается и следующего вызова:

(let ((ending-value 0))
  (do-primes (p 0 10)
    (incf ending-value p))
  ending-value)

И снова MACROEXPAND-1 может вам показать, в чем проблема. Первый вызов расширяется в следующее:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
     (ending-value 10))
    ((> ending-value ending-value))
  (print ending-value))

Некоторые реализации Lisp могут отвергуть такой код из-за того, что ending-value используется дважды в качестве имен переменных одного и того-же цикла DO. Если же этого не произойдет, то код зациклится, так как ending-value никогда не станет больше себя самого.

Второй проблемный вызов расширяется следующим образом:

(let ((ending-value 0))
  (do ((p (next-prime 0) (next-prime (1+ p)))
       (ending-value 10))
      ((> p ending-value))
    (incf ending-value p))
  ending-value)

В этом случае сгенерированный код полностью допустим, но его поведение совсем не то, что нужно вам. Так как привязка ending-value, установленная с помощью LET снаружи цикла перекрывается переменной с таким же именем внутри DO, то форма (incf ending-value p) увеличивает переменную цикла ending-value вместо внешней переменной с таким же именем, создавая другой вечный цикл7).

Очевидно, что то, что нам нужно для устранения этой «протечки» — это символ, который никогда не будет использоваться снаружи кода, сгенерированного макросом. Вы можете попытаться использовать действительно маловероятный символ, но это все равно не даст вам никаких гарантий. Вы можете также защитить себя в некоторой степени путем использования пакетов, описанных в главе 21. Но существует лучшее решение.

Функция GENSYM возвращает уникальный символ при каждом своем вызове. Такой символ никогда до этого не был прочитан процедурой чтения Lisp и, так как он не интернирован (isn’t interned) ни в один пакет, никогда не будет прочитан ею. Поэтому, вместо использования литеральных имен наподобие ending-value, вы можете генерировать новый символ при каждом раскрытии do-primes.

(defmacro do-primes ((var start end) &body body)
  (let ((ending-value-name (gensym)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

Обратите внимание, что код, вызывающий GENSYM не является частью раскрытия; он запускается как часть процедуры раскрытия макроса и поэтому создает новый символ при каждом раскрытии макроса. Это может казаться несколько странным сначала: ending-value-name является переменной, чье значение является именем другой переменной. Но на самом деле тут нет никаких отличий от параметра var, чье значение также является именем переменной. Единственная разница состоит в том, что значение var было создано процедурой чтения, когда форма макроса была прочитана, а значение ending-value-name было сгенерированно программно при запуске кода макроса.

С таким определением две ранее проблемные формы расширяются в код, который работает так, как вам нужно. Первая форма:

(do-primes (ending-value 0 10)
  (print ending-value))

расширяется в следующее:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
     (#:g2141 10))
    ((> ending-value #:g2141))
  (print ending-value))

Теперь переменная, используемая для хранения конечного значения является сгенерированным функцией gensym символом, #:g2141. Имя идентификатора, G2141, было сгенерировано с помощью GENSYM, но важно не это; важно то, что идентификатор хранит значение объекта. Сгенерированные таким образом символы печатаются в обычном синтаксисе для неинтернированных символов: с начальным #:.

Вторая ранее проблемная форма:

(let ((ending-value 0))
  (do-primes (p 0 10)
    (incf ending-value p))
  ending-value)

после замены do-primes его раскрытием будет выглядеть подобным образом:

(let ((ending-value 0))
  (do ((p (next-prime 0) (next-prime (1+ p)))
       (#:g2140 10))
      ((> p #:g2140))
    (incf ending-value p))
  ending-value)

И снова, тут нет никакой «протечки», так как переменная ending-value, связанная окружающей цикл do-primes формой LET, больше не перекрывается никакими переменными, вводимыми в коде раскрытия.

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

Этим исправлением мы устранили все «протечки» в реализации do-primes. После получения некоторого опыта в написании макросов, вы научитесь писать макросы с заранее устраненными «протечками» такого рода. На самом деле это довольно просто, если вы будете следовать следующим правилам:

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

Макросы, создающие макросы

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

На самом деле, вы уже видели один такой образец: многие макросы, как и последняя версия do-primes, начинаются с LET, который вводит несколько переменных, содержащих сгенерированные символы для использовании в раскрытии макроса. Так как это общий образец, почему бы нам не абстрагировать его с помощью его собственного макроса?

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

Предположим, вы хотите иметь возможность написать подобное:

(defmacro do-primes ((var start end) &body body)
  (with-gensyms (ending-value-name)
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

и получить do-primes, эквивалентный его предыдущей версии. Другими словами, with-gensyms должен раскрываться в LET, которая связывает каждую перечисленную переменную, ending-value-name в данном случае, со сгенерированным символом. Достаточно просто написать это с помощью простого шаблона-квазитирования.

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

Обратите внимание, как мы можем использовать запятую для подстановки значения выражения LOOP. Этот цикл генерирует список связывающих форм, каждая из которых состоит из списка, содержащего одно из переданных with-gensyms имен, а также литеральный код (gensym). Вы можете проверить, какой код сгенерирует выражение LOOP в REPL, заменив names списком символов.

CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))
((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))

После списка связывающих форм в качестве тела LET вклеивается аргумент body with-gensyms. Таким образом, из кода, который вы оборачиваете в with-gensyms, вы можете ссылаться на любое из имен переменных из списка переменных, переданного with-gensyms.

Если вы воспользуетесь macro-expand для формы with-gensyms в новом определении do-primes, то вы получите подобное:

(let ((ending-value-name (gensym)))
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
        (,ending-value-name ,end))
       ((> ,var ,ending-value-name))
     ,@body))

Выглядит неплохо. Хотя этот макрос довольно прост, очень важно ясно понимать то, когда различные макросы раскрываются: когда вы компилируете DEFMACRO do-primes, форма with-gensyms раскрывается в код, который вы только что видели. Таким образом, скомпилированная версия do-primes в точности такая же, как если бы вы написали внешний LET вручную. Когда вы компилируете функцию, которая использует do-primes, то для генерации расширения do-primes запускается код, сгенерированный with-gensyms, но сам with-gensyms при компиляции формы do-primes не нужен, так как он уже был раскрыт при компиляции do-primes.

Другой классический макрос, создающий макросы: ONCE-ONLY

Другим классическим макросом, создающим макросы, является once-only, который используется для генерации кода, вычисляющего определенные аргументы макроса только единожды и в определенном порядке. Используя once-only вы можете написать do-primes почти таким же простым способом, как исходную «протекающую» версию, следующим образом:

(defmacro do-primes ((var start end) &body body)
  (once-only (start end)
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

Однако, реализация once-only несколько запутанна для обычного пошагового объяснения, так как зависит от множества уровней квазицитирования и «раскавычивания». Если вы действительно хотите попрактиковаться в понимании макросов, вы можете попытаться разобраться, как он работает. Макрос выглядит следующим образом:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

Не только простые макросы

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

Макрос на языке C

Макрос на языке C — это фрагмент кода, которому присвоено имя. Когда имя используется где-нибудь в программе, значение макроса заменяется перед компиляцией программы. В этой статье мы подробно рассмотрим, как написать макрос на языке C.

Рассмотрим следующий код:

START

ЦЕЛОЕ n = 5;

PRINT («Значение n равно% d», n);

КОНЕЦ

Приведенный выше код не является допустимым кодом C.

Но допустим следующий код:

// Пример1.c
#define START int main () {
#define END}
#define INTEGER int
#define PRINT (A, B) printf (A, B)

START

INTEGER n = 5;

PRINT («Значение n равно% d», n);

КОНЕЦ

Перед компиляцией макросы START, INTEGER, PRINT и END были заменены их значениями, и код стал допустимым кодом C. Мы можем проверить с помощью следующей команды:

Эта команда отобразится после раскрытия всех макросов.

Теперь мы увидим разные типы макросов:

1.Макросы объектного типа:

Синтаксис:

#define macro_name macro_value

  • Макрос всегда начинается с #define
  • macro_name — определяемое пользователем имя макроса
  • macro_value — значение макроса. Это может быть что угодно, но одна строка и тело макроса заканчиваются концами этой строки. Не требует точки с запятой (;) в конце. Также учитывается пространство.

Если макрос занимает более одной строки, мы можем сделать это следующим образом:

#define macro_name macro_value1 \
macro_value2 \
macro_value3

#define MAX 200

Этот макрос выглядит как объект данных, поэтому макрос этого типа называется объектно-подобным макросом.

//Example2.c
// # include
#define MAX 200

int main ()
{
printf («МАКСИМАЛЬНОЕ значение:% d», МАКС);
возврат 0;
}

В Exapmle2.c MAX — это макрос. Из выходных данных мы видим, что MAX заменяется своим значением 200 .

2. Функциональные макросы:

Синтаксис:

#define имя_макроса () значение_макроса

имя_макроса — определяемое пользователем имя макроса.Пара скобок должна быть поставлена ​​после macro_name . Между macro_name и круглыми скобками не должно быть пробелов. Мы также можем передавать аргументы в этом типе макросов.

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

//Example3.c

#define add (x, y) x + y

int main ()
{

int a;
float b;

a = добавить (4,5);
b = добавить (2.5,3.6)

возврат 0;
}

В примере Example3.c мы видели, что, в отличие от функции C, макрос заменяет только код аргументами, не вычисляя его. Итак, мы можем передавать разные типы данных, используя один и тот же макрос.

Если мы поместим пробел между именем макроса и круглыми скобками, он будет работать так же, как объектный макрос. Ниже приведен пример C, иллюстрирующий это.

//Example4.c

#define add (x, y) x + y

int main ()
{

int a;
float b;

a = добавить (4,5);
b = добавить (2.5,3.6)
}

В примере 4 .c мы видели, что макрос add заменен на (x, y) x + y. То же, что и объектный макрос.

3. Макрос для вставки токена:
В языке C для вставки токена используется оператор ##. Используя этот оператор, мы можем объединить два действительных токена в один действительный токен.
Пример:

//Example5.c
#define MARGE (x, y) x ## y

int main ()
{

int num = MARGE (52,34);
возврат 0;
}

Если мы попытаемся вставить токен, который не сгенерирует действительный токен, компилятор C выдаст ошибку или предупреждение.

//Example6.c
#define MARGE (x, y) x ## y

int main ()
{

int num = MARGE (52, +);
возврат 0;
}

В Example6.c у нас есть сообщение об ошибке, потому что после комбинации двух токенов мы получаем недопустимый токен ’52 + ’.

4. Макрос для преобразования строк:
В языке C оператор # используется для преобразования параметра макроса в строковую константу. Когда оператор # предшествует параметру макроса, параметр преобразуется в строковый литерал.Стринги могут использоваться для макросов объектно-подобных и функциональных.
Пример:

//Example7.c
#define STRINGIZING (x) #x

int main ()
{

printf (STRINGIZING (Привет, мир));
возврат 0;
}

В примере Example7.c у нас есть строка «Hello World» с использованием макроса STRINGIZING.

Вывод:

В этой статье мы узнали обо всех типах макросов Объектных макросов , Функциональных макросов , Макро для вставки токенов , Макро для создания строк и макросов для на языке C.Теперь мы без всяких сомнений можем использовать макрос в нашей программе на языке C.

Как правильно использовать макросы в C

Одно странное явление при кодировании на C — использование макросов.

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

Использование макросов может быть крайне небезопасным, и они скрывают множество подводных камней, которые очень трудно найти. Однако, как программист на C или C ++, вы неизбежно столкнетесь с макросами в своей жизни кодирования.Даже если вы не используете их в своем собственном проекте, велика вероятность, что вы встретите их где-нибудь еще, например, в библиотеке.

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

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

Тогда в чем разница?

Ну, макросы — это функция обработки текста . После сборки программы все вхождения макроса «раскрываются» и заменяются определениями макросов.

Как это выглядит?

преобразуется в

С другой стороны, когда функции вызываются, для них выделяется новый выделенный стековый фрейм, и они действуют независимо от места, где они были вызваны.

Если вы определяете переменную var внутри функции foo, и она вызывает функцию bar , которая определяет свою собственную переменную var , то ошибки не будет, потому что переменная, определенная в bar , отличается из переменной, определенной в foo .

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

Кроме того, функции позволяют проверять типы. Это означает, что если функция ожидает строку, но вы даете ей int, вы получите ошибку (или, по крайней мере, предупреждение, в зависимости от компилятора).

Макросы, однако, просто заменяют аргумент, который вы им передаете.

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

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

Есть одно преимущество использования макросов перед функциями — эффективность.

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

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

Однако для этого есть альтернатива в C99 и C ++ — это встроенных функций .
Когда вы добавляете ключевое слово inline перед функцией, вы намекаете компилятору встроить тело функции внутрь вызывающего объекта (точно так же, как макрос).
Но что хорошо во встроенных функциях, так это то, что они могут быть отлажены . И у них есть проверка типов.
Однако ключевое слово inline — это просто подсказка для компилятора, это не строгое правило, и он может решить проигнорировать эту подсказку.
Но в gcc есть атрибут ( always_inline ), который заставляет компилятор встроить функцию.

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

Передача аргументов по умолчанию

В

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

Однако этого нет в C. Но вы можете смоделировать это с помощью такого макроса:

Этот код довольно безопасен и не скрывает никаких потенциальных ловушек, подобных тем, которые мы обсудим.

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

Использование строк отладки

Некоторые компиляторы предлагают строки отладки, которые не так полезны при использовании в функциях: __FILE__, __LINE__, __func__ .

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

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

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

Модификация синтаксиса

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

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

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

Эта техника действительно очень мощная и при правильном использовании может принести неплохие результаты.

Посмотрите, например, библиотеку Catch Test Framework.

Другие типы директив препроцессора

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

Вот некоторые из наиболее часто используемых:

#include — включить содержимое файла в ваш файл
#ifdef — условная компиляция
#define — используется для определения констант в C.

#ifdef имеет решающее значение при создании файлов заголовков.Этот макрос используется для того, чтобы файл заголовка был включен только один раз:

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

#define для констант довольно полезен, хотя некоторые проекты стараются не использовать его, заменяя его на переменные const и перечисления .

Однако у использования любой из этих альтернатив есть недостатки.

const переменные предпочтительнее макроконстант, потому что с ними выполняется проверка типов. Однако в C, это не настоящие константы.
Например, вы не можете использовать их в операторе switch-case и не можете использовать их для определения размера статического массива.

Примечание: В C ++ константные переменные являются действительными константами (вы можете использовать их в приведенных выше случаях), и настоятельно рекомендуется использовать их вместо констант #define .

Перечисления , с другой стороны, являются действительными константами. Их можно использовать в операторах switch-case и для определения размера массива.
Однако их недостаток в том, что с ними можно определять только целые числа. Вы не можете использовать их для строковых констант и констант с плавающей запятой.
Вот почему, в конце концов, константы #define — ваш единственный вариант, если вы хотите унифицированный способ создания констант в вашем коде.

Итак, как видите, макросы все еще могут быть полезны в некоторых контекстах.

Но если придется их использовать — будьте осторожны!

Ловушка 1: не ставить скобки

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

Почему это проблема?

Ну, вызовы макросов подставляются прямо в ваш код. Это может вызвать довольно неприятные побочные эффекты из-за приоритета оператора!

В этом примере мы пытаемся оценить MULTIPLY (x + 5) , что должно привести к 50.Вместо этого мы получаем результат 30.

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

НЕСКОЛЬКО (x + 5) -> (x + 5 * 5)

И, как вы знаете, умножение имеет приоритет перед сложением. Вот почему мы получаем ошибочный результат.

Как это сделать правильно?

Ловушка 2: Использование операций увеличения / уменьшения

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

Но когда вы используете это с вызовом макроса, у вас могут быть проблемы!

Допустим, у вас есть этот код:

Здесь можно ожидать, что x будет увеличиваться и будет равно 6, а результат будет 5. Неправильно:

Причина опять же — подстановка макроса:

ABS (x ++) -> ((x ++) <0? - (x ++): (x ++))

Здесь мы увеличиваем x один раз при проверке и еще раз при присвоении результата.Вот почему мы получаем неверные результаты.

Ловушка 3: передача вызовов функций

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

И это нормально. Но когда вы сделаете это с помощью макроса, у вас могут возникнуть серьезные проблемы с производительностью!

Допустим, у вас есть этот код:

Здесь у нас есть рекурсивная функция sum_chars . Мы вызываем его один раз для первой строки и второй раз для второй строки.
Но если мы передадим эти вызовы функций в качестве аргументов макроса, то мы получим 3 рекурсивных вызова вместо 2.
Для больших структур данных такой недостаток может вызвать большое узкое место в производительности.

Особенно, если макрос используется внутри рекурсивной функции !

Ловушка 4: многострочные макросы

Иногда мы пытаемся сэкономить немного места в файле, не помещая скобки при написании циклов и операторов if.

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

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

Правильный способ определить такой макрос — это окружить содержимое циклом do-while следующим образом:

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

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

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

Чтобы минимизировать проблемы, возникающие при использовании макросов, я предлагаю использовать общий стандарт для определения макросов в вашем коде.

Что именно это будет не имеет значения.

Я видел проекты, в которых все определения макросов объявлялись ВЕРХНИЙ РЕГИСТР .

В других местах я встречал образец префикса имени макроса с помощью ‘m’, , чтобы знать, что вы имеете дело с макросом.

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

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

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

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

Связанные

Предопределенные макросы

__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__

Эти макросы определены всеми компиляторами GNU, которые используют C препроцессор: C, C ++ и Objective-C. Их ценности — главные версия, дополнительная версия и уровень исправления компилятора в виде целого числа константы.Например, GCC 3.2.1 определит __GNUC__ равным 3, __GNUC_MINOR__ на 2 и __GNUC_PATCHLEVEL__ на 1. Они определяются только тогда, когда используется весь компилятор; если вы вызовете препроцессор напрямую, они не определены.

__GNUC_PATCHLEVEL__ является новым для GCC 3.0; он также присутствует в широко используемые моментальные снимки разработки, вплоть до версии 3.0 (которые определяют себя как GCC 2.96 или 2.97, в зависимости от того, какой у вас снимок).

Если все, что вам нужно знать, это компилируется ли ваша программа с помощью GCC вы можете просто протестировать __GNUC__.Если вам нужно написать код что зависит от конкретной версии, вы должны быть внимательнее. Каждый при увеличении младшей версии уровень исправления сбрасывается до нуля; каждый раз при увеличении основной версии (что случается редко) минорная версия и уровень исправления сбрасываются. Если вы хотите использовать предопределенные макросы прямо в условном выражении, вам нужно будет написать его нравится:

 / * Тест для GCC> 3.2.0 * /
#if __GNUC__> 3 || \
    (__GNUC__ == 3 && (__GNUC_MINOR__> 2 || \
                       (__GNUC_MINOR__ == 2 && \
                        __GNUC_PATCHLEVEL__> 0)) 

Другой подход — использовать предопределенные макросы для вычислите одно число, затем сравните его с порогом:

 #define GCC_VERSION (__GNUC__ * 10000 \
                     + __GNUC_MINOR__ * 100 \
                     + __GNUC_PATCHLEVEL__)
…
/ * Тест на GCC> 3.2.0 * /
#if GCC_VERSION> 30200 

Многие люди находят эту форму более простой для понимания.

__OBJC__

Этот макрос определяется со значением 1, когда компилятор Objective-C находится в использовать. Вы можете использовать __OBJC__, чтобы проверить, скомпилирован ли заголовок компилятором C или компилятором Objective-C.

__GNUG__

Компилятор GNU C ++ определяет это. Тестирование эквивалентно тестирование (__GNUC__ && __cplusplus).

__STRICT_ANSI__

GCC определяет этот макрос тогда и только тогда, когда ключ -ansi или -std переключатель, указывающий строгое соответствие какой-либо версии ISO C, был указан при вызове GCC. Он определяется как 1. Этот макрос существует прежде всего для того, чтобы направить файлы заголовков GNU libc в ограничивают свои определения минимальным набором, найденным в C 1989 г. стандарт.

__BASE_FILE__

Этот макрос расширяется до имени основного входного файла в форме строковой константы C.Это исходный файл, который был указан в командной строке препроцессора или компилятора C.

__INCLUDE_LEVEL__

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

__VERSION__

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

__OPTIMIZE__, __OPTIMIZE_SIZE__, __NO_INLINE__

Эти макросы описывают режим компиляции. __OPTIMIZE__ — это определен во всех оптимизирующих компиляциях. __OPTIMIZE_SIZE__ — это определяется, если компилятор оптимизирует размер, а не скорость. __NO_INLINE__ определяется, если никакие функции не будут встроены в их вызывающие (когда не оптимизируются или когда встраивание было специально отключено -fno-inline).

Эти макросы заставляют определенные файлы заголовков GNU предоставлять оптимизированные определения системной библиотеки с использованием макросов или встроенных функций функции. Вы не должны использовать эти макросы каким-либо образом, если не сделаете уверены, что программы будут выполняться с одинаковым эффектом независимо от того, определены. Если они определены, их значение равно 1.

__CHAR_UNSIGNED__

GCC определяет этот макрос тогда и только тогда, когда тип данных char unsigned на целевой машине.Он существует для того, чтобы стандартный заголовок файл limits.h для правильной работы. Вы не должны использовать этот макрос сам; вместо этого обратитесь к стандартным макросам, определенным в limits.h.

__REGISTER_PREFIX__

Этот макрос расширяется до одного токена (не строковой константы), который является префикс, применяемый к именам регистров ЦП на языке ассемблера для этого цель. Вы можете использовать его для написания сборки, которая может использоваться в нескольких среды. Например, в среде m68k-aout это не расширяется до нуля, но в среде m68k-coff расширяется до одного%.

__USER_LABEL_PREFIX__

Этот макрос расширяется до одного токена, который является префиксом, применяемым к пользовательские метки (символы, видимые для кода C) в сборке. Например, в окружение m68k-aout расширяется до _, но в среда m68k-coff расширяется до нуля.

Этот макрос будет иметь правильное определение, даже если -f (no-) символы подчеркивания используются, но это будет неверно, если Используются специфичные для цели параметры, которые регулируют этот префикс (например,грамм. в OSF / rose -mno-underscores параметр).

__SIZE_TYPE__, __PTRDIFF_TYPE__, __WCHAR_TYPE__, __WINT_TYPE__

Эти макросы определены для правильных базовых типов для size_t, ptrdiff_t, wchar_t и wint_t typedefs соответственно. Они существуют для создания стандартных файлов заголовков. stddef.h и wchar.h работают правильно. Вы не должны использовать эти макросы напрямую; вместо этого включите соответствующие заголовки и используйте typedefs.

__USING_SJLJ_EXCEPTIONS__

Этот макрос определяется со значением 1, если компилятор использует старую механизм на основе setjmp и longjmp для исключения умение обращаться.

Макросы

Препроцессор C

Компиляция файла исходного кода C — это двухэтапный процесс. Первым шагом в этом процессе является запуск файла исходного кода с помощью специальной программы, называемой препроцессором C. Затем выходные данные препроцессора проходят через сам компилятор C.

Препроцессор — это программа замены текста. Задача препроцессора — искать специальные директивы препроцессора , встроенные в исходный код.Эти директивы легко идентифицировать: все они начинаются с символа #.

Включает, определяет и конструирует if / else

Самая известная директива препроцессора — это команда #include. Цель этой команды — включить текст файлов заголовков в файлы исходного кода.

Когда препроцессор встречает директиву #include, например

#include 
 

он находит рассматриваемый файл заголовка и копирует все его содержимое в файл исходного кода вместо оператора #include.

Другой знакомой директивой является команда #define, которая используется для определения значений для использования в программе. Примером этого является

#define ARRAY_SIZE 100
 

Препроцессор использует команду #define для замены текста. После обнаружения #define в приведенном выше примере препроцессор заменит текст ARRAY_SIZE на значение 100 везде в файле исходного кода.

#defines иногда используется в сочетании с #ifdef.. #else .. #endif construct. Вот пример.

#ifdef ARRAY_SIZE
  int A [ARRAY_SIZE];
#еще
  int A [20];
#endif
 

Целью этой конструкции является выборочное включение кода в файл исходного кода. В приведенном выше примере, если определен символ ARRAY_SIZE , препроцессор оставит первое объявление для A в файле исходного кода и исключит второе. Если ARRAY_SIZE не определен, препроцессор удалит первое объявление для A и вместо этого вставит второе.

макросы

Другой тип директив препроцессора — это макрос. Назначение макросов — настроить что-то, что выглядит и ведет себя как объявление функции.

Вот пример. #Define ниже устанавливает макрос, который определяет что-то вроде функции возведения в квадрат.

#define SQ (x) x * x
 

Это определяет макрос SQ, который вычисляет квадрат своего аргумента. макросы работают через механизм подстановки текста. После определения макроса SQ препроцессор будет искать в программе любой текст, который выглядит как SQ () , и заменяет его текстом * .

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

если (SQ (x + 1)> 4)
 

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

если (x + 1 * x + 1> 4)
 

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

Исправление — использование некоторых защитных скобок в исходном определении макроса. Если мы изменим определение на

# определить SQ (x) (x) * (x)
 

препроцессор переведет

если (SQ (x + 1)> 4)
 

в

если ((x + 1) * (x + 1)> 4)
 

, что правильно.

основных макросов для программирования на C

Я широко использовал C в 1989–1992 годах.После этого долгое время это был C ++. С 1999 года я начал кодировать на Java, затем пришлось использовать C # для приложений Windows.

В середине 2005 года мне довелось участвовать в проекте, в котором мне приходилось использовать C, и я полностью наслаждался этим опытом. Я мог использовать некоторые из макросов C, которые я написал еще в начале 1990-х, а также получил возможность реализовать некоторые из них, которые я задумал, но не реализовал.

IMHO, знание C ++ и Java позволяет писать лучший код на C и даже позволяет использовать концепции ООП.Опыт в C / C ++ позволяет избежать проблем с управлением памятью при кодировании на Java и C #, по-настоящему оценить удобство, обеспечиваемое сборщиком мусора, и в то же время внимательно следить за производительностью приложения при запуске потока сборщика мусора. выполнение. Хотя я не упускаю указатели C и AutoPointers C ++, я определенно скучаю по перегрузке операторов в Java. Рад, что в C # поддерживается перегрузка операторов, а ее функция делегирования, безусловно, заслуживает одобрения.

Позвольте мне начать делиться с вами макросами C и надеюсь, вы найдете их полезными и информативными.

Java и C # имеют функции « длина » для получения длины любого типа массива. У нас может быть аналогичная функциональность в C, но она будет работать только для массивов фиксированного размера, а не для динамических массивов, созданных с помощью malloc или calloc.

#define GET_ARRAY_LEN (имя_массива) (sizeof (имя_массива) / sizeof ((имя_массива) [0])) 

MIN и MAX — это часто используемые макросы, и в некоторых ситуациях они могут не определяться.Их удобно иметь, если их нет в наличии.

#ifndef MIN
#define MIN (n1, n2) ((n1)> (n2)? (n2): (n1))
#endif

#ifndef MAX
#define MAX (n1, n2) ((n1)> (n2)? (n1): (n2))
#endif 

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

#define ALIGN_SIZE (sizeToAlign, PowerOfTwo) \
        (((sizeToAlign) + (PowerOfTwo) - 1) & ~ ((PowerOfTwo) - 1))

#define IS_SIZE_ALIGNED (sizeToTest, PowerOfTwo) \
        (((sizeToTest) & ((PowerOfTwo) - 1)) == 0) 

Второй макрос эквивалентен ((sizeToTest% PowerOfTwo) == 0) .Макрос избегает дорогостоящего оператора по модулю и выполняет то же самое с помощью побитового оператора. Только если знаменатель представляет собой точную степень двойки, то для получения остатка можно использовать побитовый оператор И .

Первый макрос эквивалентен (sizeToAlign + PowerOfTwo - 1) / PowerOfTwo * PowerOfTwo . Макрос избегает целочисленного деления, а также умножения. Современные оптимизирующие компиляторы должны иметь возможность делать то же самое в большинстве случаев, но зачем рисковать, если мы можем сделать это без особого труда.

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

Другие полезные макросы, связанные со структурой struct , — это ALLOC_STRUCT и INIT_STRUCT . Я обнаружил, что они делают код легко читаемым, сокращают количество нажатий клавиш для ввода и уменьшают вероятность ошибок.


#define GET_FIELD_OFFSET (StructName, FieldName) \
        ((короткое) (длинное) (& ((StructName *) NULL) -> FieldName))

#define GET_FIELD_PTR (pStruct, nOffset) \
        ((void *) (((char *) pStruct) + (nOffset)))


#define ALLOC_STRUCT (StructName) ((StructName *) malloc (sizeof (StructName)))


#define INIT_STRUCT (pStruct) (memset (pStruct, '\ 0', sizeof (* (pStruct)))) 

Вот несколько простых, но полезных макросов.Я хотел бы упомянуть мою любимую строчку из самой обожаемой и известной книги «Язык программирования C» Брайана В. Кернигана и Денниса М. Ричи. Цитата может быть неточной, но я постараюсь передать то, что понял.
«Просто всегда элегантно. Элегантно всегда просто. Но не проще». .

Для тех, кто плохо знаком с C, «Книга ответов на C» обязательна, и она содержит решения к упражнениям, приведенным в первой книге. Чтобы упомянуть еще несколько книг, которые мне понравилось читать, это «Ловушки и подводные камни C», «Написание твердого кода», «Жемчужины программирования», «Еще несколько жемчужин программирования», «Структуры данных и программы на языке C» и т. Д.

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

#define IS_ODD (число) ((число) & 1)

#define IS_EVEN (число) (! IS_ODD ((число)))


#define IS_BETWEEN (numToTest, numLow, numHigh) \
        ((символ без знака) ((numToTest)> = (numLow) && (numToTest) <= (numHigh))) 

Следующий макрос заимствован из MFC (Microsoft Foundation Classes) для подавления предупреждений компилятора о неиспользуемых параметрах в теле функции.


#define НЕИСПОЛЬЗУЕМЫЕ (ParamName) \
        ((void) (0? ((ParamName) = (ParamName)): (ParamName))) 

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

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

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


#define BEGIN_BLOCK {


#define END_BLOCK} 

А теперь позвольте мне представить макросы, связанные с порядком байтов, которые мне очень понравилось реализовывать. Архитектура памяти, используемая большинством машин CISC, является Little-Endian, а большинство архитектур RISC используют Big-Endian, и вы можете получить сравнение архитектур ЦП здесь. Эти слова Little-Endian и Big-Endian заимствованы из классического произведения Джонатана Свифта «Путешествия Гулливера». Это имена двух враждующих фракций лилипутов, фигурирующих в этой вневременной сатире.

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

Java по умолчанию использует формат Big-Endian при записи или чтении из файла. Таким образом, если один и тот же файл читается на разных машинах только приложениями Java, проблем нет. Однако, если какой-либо другой язык, такой как C или C ++, используется для чтения файла, написанного приложением Java, или наоборот, то нужно знать и обращать внимание на порядок байтов.

Перейдем к макросам. Первые два макроса IS_LITTLE_ENDIAN и IS_BIG_ENDIAN возвращают ИСТИНА или ЛОЖЬ в зависимости от архитектуры памяти текущего компьютера.Затем идет самый важный макрос IS_DEFAULT_ENDIAN , где нам нужно решить и установить, какой из них мы хотим использовать по умолчанию.

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

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

Пожалуйста, не стесняйтесь использовать все социальные кнопки, отображаемые под этой статьей. Будем признательны, если вы оставите свои комментарии, мнения или предложения, а пока я начну готовиться к тому, чтобы принести вам еще один пост в блоге.


#define IS_LITTLE_ENDIAN () (((* (короткое *) "21") & 0xFF) == '2')


#define IS_BIG_ENDIAN () (((* (короткое *) "21") & 0xFF) == '1')


#define IS_DEFAULT_ENDIAN () IS_LITTLE_ENDIAN ()


#define REVERSE_BYTE_ARRAY (массив байтов, размер) \
        если (! IS_DEFAULT_ENDIAN ()) \
        {\
            int _i, _j; \
            char _cTmp; \
            for (_i = 0, _j = (Размер) - 1; _i <_j; _i ++, _j--) \
            {\
                _cTmp = ((char *) (ByteArray)) [_i]; \
                ((char *) (ByteArray)) [_i] = ((char *) (ByteArray)) [_j]; \
                ((char *) (ByteArray)) [_j] = _cTmp; \
            } \
        }


#define CONVERT_NUM (n) REVERSE_BYTE_ARRAY ((& (n)), sizeof (n))


#define CONVERT_NUM16 (n) ((void) (IS_DEFAULT_ENDIAN ()? (n) \
        : ((n) = ((((n) & 0x00FF) << 8) | (((n) & 0xFF00) >> 8)))))


#define CONVERT_NUM32 (n) ((void) (IS_DEFAULT_ENDIAN ()? (n) \
        : ((n) = ((((n) & 0x000000FF) << 24) | (((n) & 0x0000FF00) << 8) \
        | (((n) & 0xFF0000) >> 8) | (((n) & 0xFF000000) >> 24)))))


#define CONVERT_FLOAT (f) CONVERT_NUM32 ((* (длинное *) & (f)))


#define CONVERT_DOUBLE (d) CONVERT_NUM (d)


#define CONVERT_NUM64 (n) CONVERT_NUM (n) 
Макросы

- Учебники по C - Sanfoundry

Этот учебник по C объясняет макросы на языке C с примерами.

Макросы похожи на функции, но не являются настоящими функциями. Например,

 #define MAX (a, b) ((a)> (b)? (A): (b)) 

Давайте использовать его в программе на C,

 #include 
#define MAX (a, b) ((a)> (b)? (a): (b))

int main (пусто)
{
    int x = 10, y = 15;
    поплавок u = 2.0, v = 3.0;
    двойной s = 5, t = 5;

    printf ("Максимум двух целых чисел% d и% d равен:% d \ n", x, y, MAX (x, y));
    printf ("Макс. из двух чисел с плавающей запятой% .2f и% .2f:% .2f \ n", u, v, MAX (u, v));
    printf ("Максимум два двойных%.2lf и% .2lfis:% lf \ n ", s, t, MAX (s, t));

    возврат 0;
} 

Обратите внимание на следующий вывод:

 Максимальное количество двух целых чисел 10 и 15: 15
Максимальное количество поплавков 2,00 и 3,00: 3,00
Максимум два двойных 5.00 и 5.00is: 5.000000 

Обратите внимание, что тот же макрос MAX (a, b) оценивается как большее из двух целых чисел, двух чисел с плавающей запятой, двух чисел двойной точности и т. Д. Это означает, что макросы не имеют типа. Если бы мы использовали вместо этого функции, нам потребовалось бы три, по одной для каждого типа значений. Кроме того, есть накладные расходы на вызов и возврат функций.

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

Sanfoundry Global Education & Learning Series - Учебники по 1000 C.

Если вы хотите просмотреть все учебные пособия по C, перейдите к учебным пособиям по C.
Оставить комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *