Для чего применяется директива препроцессора include
Для чего применяется директива препроцессора include
Подключаемый файл это файл, содержащий определения функций и переменных, а также макроопределения вместе с некоторыми исходными файлами. Для использования в программе подключаемых файлов применяется директива препроцессора ‘#include’.
Обычно подключаемые файлы заканчиваются на ‘.h’ и следует избегать использования других стандартов.
Как файлы пользователя, так и системные файлы включаются в программу с использованием директивы препроцессора ‘#include’. Она имеет три модификации:
Синтаксис такой модификации директивы ‘#include’ довольно специфичен, потому как комментарии внутри ‘ ‘ не распознаются. Поэтому в строке ‘#include *y>’ последовательность символов ‘*’ не начинает комментарий, а указанная директива включает в программу файл с именем ‘x/*y’.
Аргумент FILE не может содержать символа ‘>’, хотя он может содержать символ ‘
Аргумент FILE не может содержать символов ‘»‘. Символы backslash интерпретируются как отдельные символы, а не начало escape последовательности. Таким образом, директива ‘#include «x\n\\y»‘ указывает имя файла, содержащего три символа backslash.
Эта возможность позволяет определять макросы, что дает возможность изменять имена подключаемых файлов. Эта возможность, например, используется при переносе программ с одной операционной системы на другие, где требуются разные подключаемые файлы.
Директива ‘#include’ указывает С препроцессору обработать указанный файл перед обработкой оставшейся части текущего файла. Информация, выдаваемая препроцессором, содержит уже полученные данные, за которыми следуют данные, получаемые при обработке подключаемого файла, а за которыми, в свою очередь, следуют данные, получаемые при обработке текста, следующего после директивы ‘#include’. Например, дан следующий подключаемый файл ‘header.h’:
Подключаемый файл может содержать начало или окончание сиснтаксической конструкции, такой как определение функции.
Срока, следующая за директивой ‘#include’ всегда является пустой и добавляется С препроцессором даже если подключаемый файл не содержит завершающий символ перевода строки.
Часто случается, что подключаемый файл включает в себя другой файл. Это может привести к тому, что отдельный файл будет подключаться неоднократно, что может привести к возникновению ошибок, если файл определяет типы структур или определения типов. Поэтому следует избегать многократного подключения файлов.
Обычно это достигается путем заключения в условие всего содержимого этого файла, как показано ниже:
Препроцессор GNU C построен таким образом, что обработке подключаемого файла он проверяет наличие определенных конструкций и наиболее рационально их обрабатывает. Препроцессор специально отмечает полное вложение файла в условие ‘#ifndef’. Если в подключаемом файле содержится директива ‘#include’, указывающая на обрабатываемый файл, или макрос в директиве ‘#ifndef’ уже определен, то обрабатываемый файл полностью игнорируется.
Существует также специальная директива, указывающая препроцессору, что файл должен быть включен не более одного раза. Эта директива называется ‘#pragma once’. Она использовалась в дополнение к директиве ‘#ifndef’ и в настоящее время она устарела и не должна прменяться.
В объектно ориентированном языке С существует модификация директивы ‘#include’, называемая ‘#import’, которая используется для вкючения файла не более одного раза. При использовании директивы ‘#import’ вместо ‘#include’ не требуется наличия условных оборотов для предотвращения многократной обработки файла.
«Наследование» это то, что происходит, когда какой либо объект или файл образует некоторую часть своего содержимого путем виртуального копирования из другого объекта или файла. В случае подключаемых С файлов наследование означает, что один файл включает другой файл, а затем заменяет или добавляет что-либо.
Если наследуемый подключаемый файл и основной подключаемый файл имеют различные имена, то такое наследование называется прямым. При этом используется конструкция ‘#include «BASE»‘ в наследуемом файле.
Иногда необходимо чтобы у наследуемого и основного подключаемого файла были одинаковые имена.
Например, предположим, что прикладная программа использует системный подключаемый файл ‘sys/signal.h’, но версия файла ‘/usr/include/sys/signal.h’ на данной системе выполняет того, что требуется в прикладной программе. Будет удобнее определить локальную версию, возможно с именем ‘/usr/local/include/sys/signal.h’ для замены или добавления к версии, поставляемой с системой.
Это можно выполнить с применением опции ‘-I.’ при компиляции, а также созданием файла ‘sys/signal.h’ который выполняет требуемые программе функции. Но сделать так, чтобы этот файл включал стандартный файл ‘sys/signal.h’ не так просто. При включении строки ‘#include ‘ в этот файл произойдет подключение новой версии файла, а не стандартной системной версии. Это приведет к рекурсии и ошибке при компиляции.
При использовании директивы `#include ‘ нужный файл будет найден, но этот способ является не эфективным, так как содержит полный путь к системному файлу. Это может отразиться на содержании системы, так как это означает, что любые изменения местоположения системных файлов потребуют дополнительных изменений где-либо еще.
Более эффективным решением этой проблемы является применение директивы ‘#include_next’, которая используется для подключения следующего файла с таким же именем. Эта директива функционирует также как и директива ‘#include’ за исключением поиска требуемого файла. Она начинает поиск списка каталогов подключаемых файлов после каталога, где был найден текущий файл.
Препроцессор
Препроцессор является обязательным компонентом компилятора языка Си. Препроцессор обрабатывает исходный текст программы до ее непосредственной компиляции. Результатом работы препроцессора является полный текст программы, который передается на компиляцию в исполняемый файл.
Для управления препроцессором применяются директивы, каждая из которых начинается с символа решетки # и располагается на отдельной строке. Препроцессор просматривает текст программы, находит эти директивы и должным образом обрабатывает их.
Мы можем использовать следующие директивы:
#define : определяет макрос или препроцессорный идентификатор
#undef : отменяет определение макроса или идентификатора
#ifdef : проверяет, определен ли идентификатор
#ifndef : проверяет неопределенности идентификатор
#include : включает текст из файла
#if : проверяет условие выражение (как условная конструкция if)
#else : задает альтернативное условие для #if
#endif : окончание условной директивы #if
#elif : задает альтернативное условие для #if
#line : меняет номер следующей ниже строки
#error : формирует текст сообщения об ошибке трансляции
#pragma : определяет действия, которые зависят от конкретной реализации компилятора
# : пустая директива, по сути ничего не делает
Рассмотрим основные из этих директив.
Директива #include. Включение файлов
Поиск файла производится стандартных системных каталогах. Вообще есть стандартный набор встроенных заголовочных файлов, который определяется стандартом языка и которые мы можем использовать:
assert.h : отвечает за диагностику программ
complex.h : для работы с комплексными числами
ctype.h : отвечает за преобразование и проверку символов
errno.h : отвечает за проверку ошибок
fenv.h : для доступа к окружению, которое управляет операциями с числами с плавающей точкой
float.h : отвечает за работу с числами с плавающей точкой
inttypes.h : для работы с большими целыми числами
iso646.h : содержит ряд определений, которые расширяют ряд логических операций
limits.h : содержит предельные значения целочисленных типов
locale.h : отвечает за работу с локальной культурой
math.h : для работы с математическими выражениями
setjmp.h : определяет возможности нелокальных переходов
signal.h : для обработки исключительных ситуаций
stdalign.h : для выравнивания типов
stdarg.h : обеспечивает поддержку переменного числа параметров
stdatomic.h : для выполнения атомарных операций по разделяемым данным между потоками
stdbool.h : для работы с типом _Bool
stddef.h : содержит ряд вспомогательных определений
stdint.h : для работы с целыми числами
stdio.h : для работы со средствами ввода-вывода
stdlib.h : содержит определения и прототипы функций общего пользования
stdnoreturn.h : содержит макрос noreturn
string.h : для работы со строками
tgmath.h : подключает math.h и complex.h плюс добавляет дополнительные возможности по работе с математическими вычислениями
threads.h : для работы с потоками
time.h : для работы с датами и временем
uchar.h : для работы с символами в кодировке Unicode
wchar.h : для работы с символами
wctype.h : содержит дополнительные возможности для работы с символами
Однако стоит отметить, что в различных средах к этому набору могут добавляться дополнительные встроенные заголовочные файлы для тех или иных целей, например, для работы с графикой.
Здесь просто определена одна переменная. Теперь подключим этот файл в программу:
При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла main.c. Но опять же отмечу, важно, что в данном случае файл main.c располагается в одной папке с главным файлов программы.
Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могули бы его подключить в файл исходного кода:
Этот пример также будет работать в GCC, однако как уже было рассмотрено выше, подключение файла main.h для GCC необязательно.
#include Директива (C/C++)
Указывает препроцессору включить содержимое указанного файла в точку, где отображается директива.
Синтаксис
#include » путь — спецификация «
#include путь — спецификация >
Remarks
Можно организовать константы и определения макросов в включаемые файлы (также известные как заголовочные файлы), а затем использовать #include директивы для добавления их в любой исходный файл. Включаемые файлы также позволяют внедрять объявления внешних переменных и сложных типов данных. Типы можно определять и именовать только один раз во включаемом файле, созданном с этой целью.
Путь-Spec — это имя файла, которому при необходимости может предшествовать Спецификация каталога. Имя файла должно указывать на существующий файл. Синтаксис инструкции path-Spec зависит от операционной системы, в которой компилируется программа.
Обе синтаксические формы приводят к #include замене директивы всем содержимым указанного файла. Различие между двумя формами — это порядок путей, которые препроцессор ищет при неполном указании пути. В приведенной ниже таблице показывается различие между этими формами синтаксиса.
Форма синтаксиса | Действие |
---|---|
Форма в кавычках | Препроцессор ищет включаемые файлы в следующем порядке: 1) в том же каталоге, что и файл, содержащий #include инструкцию. 2) в каталогах открытых в данный момент файлов включения в порядке, в котором они были открыты. Поиск начинается в каталоге родительского включаемого файла, а затем выполняется в каталогах всех включаемых файлов-прародителей. 3) вдоль пути, указанного в каждом /I параметре компилятора. 4) вдоль путей, указанных в INCLUDE переменной среды. |
Форма с угловыми скобками | Препроцессор ищет включаемые файлы в следующем порядке: 1) вдоль пути, указанного в каждом /I параметре компилятора. 2) при компиляции происходит в командной строке вместе с путями, заданными INCLUDE переменной среды. |
Как только препроцессор найдет файл с заданным именем, поиск останавливается. При заключении полной, неоднозначной спецификации пути для включаемого файла между двойными кавычками ( » » ) препроцессор выполняет поиск только по этой спецификации пути и игнорирует стандартные каталоги.
Если имя файла, заключенное в двойные кавычки, является неполным указанием пути, препроцессор сначала выполняет поиск в каталоге родительского файла. Родительский файл — это файл, содержащий #include директиву. Например, если включить файл с именем file2 в файл с именем file1, то файл file1 будет родительским.
Включаемые файлы могут быть вложенными: #include директива может находиться в файле с именем другой #include директивы. Например, file2 может включать файл3. В этом случае file1 будет по-прежнему являться родителем file2, но это было бы бабушке файл3.
в среде разработки Visual Studio INCLUDE переменная среды игнорируется. Вместо них используются значения, указанные в свойствах проекта для каталогов включения. дополнительные сведения о настройке каталогов включения в Visual Studio см. в разделе включаемые каталоги и дополнительные каталоги включаемыхданных.
В приведенном ниже примере демонстрируется включение файлов с помощью угловых скобок:
В следующем примере демонстрируется включение файлов, заданных в кавычках:
В примере добавляется содержимое файла, указанного defs.h в исходной программе. Кавычки означают, что препроцессор сначала попытается найти этот файл в каталоге, содержащем родительский исходный файл.
Для включаемых файлов поддерживается до 10 уровней вложения. После завершения обработки вложенного объекта #include препроцессор сохраняет вложенный родительский файл в исходный исходный файл.
Только для систем Майкрософт
Представим себе следующую команду:
Эта инструкция действуют точно так же. Если найти файл в обоих наборах каталогов не удастся, возникает неустранимая ошибка компилятора.
Если имя файла указано полностью для включаемого файла с путем, содержащим двоеточие (например, F:\MSVC\SPECIAL\INCL\TEST.H ), препроцессор следует за путем.
КОНЕЦ Только для систем Майкрософт
Препроцессор C
Си препроцессор представляет собой макро язык, который используется для преобразования программы до того как она будет скомпилирована. Причем сама программа может быть не обязательно на Си, она может быть на С++, Objective-C или даже на ассемблере. В общем препроцессор представляет собой примитивный как-бы функциональный язык, с помощью которого можно делать вполне интересные вещи.
Как работает препроцессор.
Для понимания работы препроцессора важно осознавать уровень абстракций с которыми он работает. Основным понятием в препроцессоре является токен (token) — это, грубо говоря последовательность символов, отделённая разделителями, похоже на идентификатор в Си, но значительно шире. В общем в препроцессоре есть только директивы, токены, строковые и числовые литералы и выражения, еще он понимает комментарии (просто их игнорирует). Упрощенно говоря, препроцессор работает с текстовыми строчками, умеет их склеивать, превращать в строковый литерал, выполнять макроподстановку, и подключать файлы.
Директивы препроцессора.
Директивы — это специальные команды, которые препроцессор распознаёт и выполняет. Все директивы начинаются со знака #. Если первый непробельный символ в строке это — #, то препроцессор будет пытаться распознать в ней свою директиву.
Существуют следующие директивы:
— Подключение файлов: #include, #include_next.
— Условная компиляция: #if, #ifdef, #ifndef, #else, #elif and #endif.
— Диагностика: #error, #warning, #line.
— Дополнительная информация компилятору: #pragma
— Макроопределения: #define
Подключение файлов.
Первая директива, которая всем встречается при изучении языка Си — это #include. Записывается так:
Встретив в исходнике эту директиву, препроцессор заменяет её на содержимое файла, имя которого указанно в параметре. Различие между первой и второй формой записи состоит в том, где в первую очередь, препроцессор будет искать указанный файл. В первом случае он сначала будет искать в каталогах с системными заголовками. Во втором — в том-же каталоге, где находится компилируемый исходник. Грубо говоря, при подключении системных/стандартных заголовков нужно имя файла писать в угловых скобках, а для своих — в кавычках.
Мало кто знает, но есть ещё одна директива для включения файлов — #include_next. Записывается она также как и обычный #include, но ее поведение несколько отличается. Дело в том, что препроцессор ищет подключаемые заголовки по многим путям, и бывает, что искомый файл есть сразу в нескольких каталогах. В случае применения директивы #include, он подключает первый попавшийся файл с совпавшим именем. В случае #include_next — будет подключен первый файл с совпавшим именем, который еще не включался в эту единицу трансляции, то есть следующий еще не подключенный. Причем применять #include_next можно только в этих самых заголовках с совпадающими именами, применённая в.с файле эта директива ведёт себя как обычный #include. Таким образом, если в каждом из заголовков с одинаковыми именами применить #include_next, то конечном итоге, они все будут подключены.
Ещё одна интересная особенность директивы #include то, что в ней тоже выполняется макроподстановка. То есть, параметра в ней можно использовать любой макрос, который развернётся в имя файла в одной из двух допустимых форм(в кавычках или в угловых скобках). Например:
Условная компиляция
Применяется, когда в зависимости от значения различных макросов, нужно компилировать, или нет, тот или иной кусок кода, или установить другие макросы.
Где условие — это выражение препроцессора. Это может быть любая комбинация макросов, условий и целочисленных литералов, которая в результате макроподстановки превратится в выражение состоящее только из целочисленных литералов, арифметических операций и логических операторов. Так-же здесь ещё можно использовать единственный «макрооператор» — defined — он превращается в 1, если его операнд определён, и 0 — если нет.
__AVR__ и __ICCAVR__ — это специальные предопределённый макросы, позволяющие определить используемый компилятор. Соответственно для каждого компилятора существует предопределённый макрос, который позволяет его однозначно идентифицировать.
Как уже говорилось, препроцессор работает на уровне отдельных токенов — текстовых строчек, их значение препроцессору безразлично, и он ничего не знает о правилах и грамматике целевого языка. Поэтому в директивах условной компиляции нельзя использовать никакие конструкции языка Си. Например:
В обоих приведённых примерах условия будут всегда ложны и содержимое #if блоков не выполнится. Препроцессор не знает ничего, ни о структурах и их размере, ни о переменных — они-ж не макросы. По этому в первом случае нужно использовать static_assert, реализованный средствами самого Си. А вот во втором случае можно извернутся так:
Условия могут быть сложными и содержать в себе макросы, которые будут полностью развёрнуты перед вычислением условия:
В данном примере блок #if выполнится если макрос BUFFER_SIZE имеет значение кратное степени двойки и если определен макрос OPTIMIZE_FOR_POWER_OF_2. Конструкция IS_POWER_OF_2(BUFFER_SIZE) после макроподстановки развернется в выражение ((16) & (16)-1 == 0), которое препроцессор легко вычислит.
Для конструкции типа #if defined есть сокращенная форма: #ifdef. Она во всём эквивалентна полной форме, за исключением того, что в сокращенной форме нельзя комбинировать несколько условий.
Также директивы условной компиляции часто используются для предотвращения повторного включения заголовочных файлов (include guard):
Эта конструкция гарантирует, что все определения из заголовка будут включены только один раз в единицу трансляции.
Диагностика.
В предыдущих примерах мы уже встретились с одной диагностической директивой — #error. Назначение её предельно просто — остановить компиляцию с сообщением об ошибке, указанном после директивы. Её можно использовать совместно с директивами условной компиляции для того, чтоб убедиться установлен ли какой-то важный макрос и, что он имеет правильное значение.
Также существует директива #warning, аналогична #error, но не прерывает компиляцию, а выдаёт предупреждение.
Директива #line служит для задания номеров строк и имени файла, показываемых в сообщениях об ошибках и возвращаемые специальными макросами __LINE__ и __FILE__. Например:
При этом в сообщениях об ошибках мы увидим следущее:
Надо учитывать, что такая конструкция собьёт столку любую IDE (и человека тоже) и найти место ошибки будет очень не просто. Однако этот трюк можно использовать, чтоб указать на ошибку, возникшую где-то далеко от места, где мы ее проверяем, например на какой-то важный макрос, определённый в другом файле и имеющий не правильное значение. Надо только точно знать где он расположен.
#pragma
Макроопределения
Теперь переходим к интересному, собственно к макросам. существуют два типа макросов: макрос-объект(object-like macro) и макрос-функция(function-like macro), оба типа объявляются с помощью директивы #define. Рассмотрим сначала макросы-объекты. Объявляются они как:
#define ИМЯ_МАКРОСА [замещающий текст]
Всё, что идёт после имя макроса до конца строки является замещающим текстом.
При дальнейшей обработке файла, если препроцессор находит имя макроса, он заменяет его на соответствующий замещающий текст — это называется макроподстановка. Если в замещающем тексте макроса встречаются имена других макросов, препроцессор выполнит макроподстановку для каждого из них, и так далее, пока не будут развёрнуты все известные на данный момент макросы.
Когда препроцессор будет обрaбатывать строчку:
char buffer[DOUBLE_BUFFER];
Сначала будет выполнена первая макроподстановка и токен DOUBLE_BUFFER будет заменен на EXTRA_BUFFER * 2. Тут-же будет выполнена вторая макроподстановка и токен EXTRA_BUFFER заменется на (BUFFER_SIZE +10), потом BUFFER_SIZE заменется на 32. В результате вся строчка после препроцессинга будет выглядеть так:
Здесь становится понятно, зачем были нужны скобки в макросе EXTRA_BUFFER, без них результирующее выражение получилось бы таким:
А это явно не то, что мы хотели получить. Отсюда правило:
Если макрос содержит какое-то выражение, то оно обязательно должно быть заключено в скобки, иначе могут происходить всякие неочевидные вещи.
Также важно понимать, что препроцессор сам ничего не вычисляет (кроме как в условных директивах #if), он просто склеивает текстовые строчки.
А что будет, если какой-то макрос будет ссылаться сам на себя, непосредственно, и косвенно через другие макросы? Ничего не будет, рекурсии не получится, как только препроцессор просечет рекурсию, он прекратит макроподстановку макроса её вызвавшего и оставит его как есть. Например:
При этом будет определён и символ препроцессора flags и переменная flags. такую особенность часто используют для того, чтобы иметь возможность проверить наличие переменной(или любого другого идентификатора) с помощью директив условной компиляции #ifdef/#else/#endif:
// если флаги определены
Хотя я бы не рекомендовал использовать такой приём без крайней необходимости, так как он сильно затрудняет понимание программы и чреват ошибками, поскольку мы по сути пишем две версии программы в одном наборе исходников со всеми вытекающими последствиями. Ведь есть-же системы контроля версий!
Предопределённые макросы
У каждого компилятора есть множество предопределённых макросов, есть стандартные — общие для всех: gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html#Standard-Predefined-Macros
Есть специфичные для каждого отдельного компилятора, например у gcc:
gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html#Common-Predefined-Macros
И даже для каждой поддерживаемой платформы, например для avr-gcc, список всех предопределённых макросов(кроме контекстно зависимых, таких как __LINE__ и т.д) можно получить набрав в командной строке:
Соответственно, вместо atmega16 писать интересующий контроллер.
В других компиляторах предопределённые макросы ищутся в соответствующей документации.
Все эти макросы могут использоваться для определения платформы, для которой компилируется программа, используемого языка (Си, Си++ или ассемблер) и различных особенностей целевой архитектуры.
Также есть макросы предназначенные в основном для отладки: __FILE__, __LINE__ и __FUNCTION__. __FILE__ разворачивается в строковый литерал, содержащий имя обрабатываемого файла. __LINE__ — целочисленный литерал означающий номер текущей строки. __FUNCTION__ — имя текущей функции. Надо заметить, что макрос __FUNCTION__ разворачивается всё-таки не препроцессором а компилятором — препроцессор ничего не знает о функциях в языке Си. Также надо учитывать, что значения __LINE__ и __FILE__ могут изменяться с помощью директивы #line.
Типичное использование макросов __LINE__, __FILE__ и __FUNCTION__:
При этом вызов функции MyError превратится во что-то такое:
Макросы-функции
Второй вид макросов — это макро-функции (function-like macros). Определяются они с помощью той-же директивы #define, после которой (сразу без пробелов) в круглых скобках идёт список разделённых запятыми аргументов:
Макрос SQR предназначен вычислять квадрат переданного ему выражения, в приведённом примере SQR(b) развернётся в (b * b). Вроде-бы нормально, но если этому макросу передать более сложное выражение
,
то он развернётся совсем не в то, что нужно:
Очевидно, что умножение выполнится первым и это у нас уже далеко не квадрат.
Поэтому все аргументы макросов используемые в математических и не только выражениях надо обязательно заключать в скобки:
Однако и этот вариант не свободен от недостатков, например:
Переменная b будет инкрементирована два раза. И у этого недостатка есть решения гибкие и не очень, стандартные и нет, но о них говорить не будем. В данном примере гораздо лучше применить встраиваемую (inline) функцию, она свободна от недостатков макросов:
У макросов-функций есть интересная особенность — макроподстановка в них выполняется два раза. Первый раз — для каждого из параметров до того, как они будут подставлены в тело макроса. Второй раз — для всего тела макроса после подстановки в него параметров. В большинстве случаев это не имеет особого значения. Подробнее об этом можно прочитать здесь:
gcc.gnu.org/onlinedocs/cpp/Argument-Prescan.html#Argument-Prescan
В макро-функциях можно использовать два специальных макро-оператора: # и ##. Первый превращает свой операнд в строковый литерал:
Вызов PRINT_VAR в данном случае превратится в
При этом будет напечатана строка: my_var = 10. Здесь для склеивания форматной строки использован тот факт, что две строки разделённые лишь пробельными символами компилятор считает одной строкой: «%s = %» «d».
Макро-оператор ## склеивает два токена в один токен, для которого после будет выполнена макроподстановка:
Применять эти макро-операторы можно только к параметрам макросов. Причем для параметров к которым они применены макроподстановка будет применена только один раз — для полученного результата. То есть параметр PORT_LETTER не будет отдельно сканироваться на наличие в нем макросов. Почему макрос SET_PIN состоит из двух уровней объясняется ниже.
Теперь, допустим, нам нужен макрос, который склеивает идентификатор из двух кусков:
Если параметра этого макроса непосредственно, те токены, что нам нужно склеить, как в примере выше, то всё сработает как надо. Если-же это макросы, которые сначала нужно раскрыть, то придется вводить еще один вспомогательный макрос, который сначала развернёт параметры и передаст их следующему макросу:
Из-за того, что для параметров, для которых применена конкатенация, не производится макроподстановка, в препроцессорном метапрограммировании часто приходится применять такие двухуровневые макросы: один — для развёртывания параметров, второй — делает непосредственную работу.
Макро-функции можно передать имя другой макро-функции в качестве параметра и, соответственно, вызвать её:
Практический пример препроцессорного метапрограммирования
В качестве примера рассмотрим генерацию таблицы для вычисления контрольной суммы CRC16. Функция для вычисления CRC16 для каждого байта выглядит так:
Где newchar — очередной байт сообщения для которого вычисляем CRC,
crcval — предыдущее значение CRC.
сrcTable — таблица из 256 значений, которую нам надо сгенерировать.
Функция возвращает новое значение контрольной суммы.
Первоначальная идея была и вовсе вычислять CRC16 от строкового литерала с помощью препроцессора, чтобы можно было реализоват «switch» по CRC16 от строки, с удобочитаемыми метками. Но только на препроцессоре это сделать не получилось из-за степенной сложности генерируемых выражений — компилятору банально не хватает памяти, чтоб посчитать таким образом CRC16 для двух символов. На шаблонах С++ это можно сделать без проблем.
Елементы таблицы сrcTable можно вычислить с помощью такой функции:
Где v — индекс в таблице,
polynom — полином контрольной суммы, в данном примере будем использовать значение 0x8408, соответствующее стандарту CRC-CCITT.
Теперь нужно этот алгоритм реализовать с помощью препроцессора. Как быть с циклом? В препроцессоре нет ни циклов ни рекурсии. Прийдётся цикл развернуть вручную:
Теперь, вызывая макрос CRC_TABLE_8 мы получаем константное выражение для одного элемента таблицы. Выражение это, кстати, очень длинное порядка 200-400 тысяч символов! Это происходит потому, что каждый(кроме первого) макрос CRC_TABLE_x вызывает 3 макроса более нижнего уровня, а ведь препроцессор сам выражения не вычисляет, оставляя это компилятору. И получается в результате длинна такого выражения порядка 3 в восьмой степени помножить на длинны выражения низшего уровня. Но ничего, это компилятор еще прожевывает. Теперь нужно сгенерировать саму таблицу:
#define CRC_POLYNOM 0x8408
Можно, конечно оставить и так, но есть решение получше, называется — библиотека Boost preprocessor. В ней имеется много всяких полезняшек, в частности есть макрос BOOST_PP_REPEAT, который повторяет заданное количество раз макрос, переданный ему в качестве параметра. С использованием BOOST_PP_REPEAT геерацию таблицы можно написать так:
Выглядит уже вполне неплохо. Макрос, который будет повторяться в BOOST_PP_REPEAT, должен иметь три параметра. Первый уровень вложенности повторения, если мы будем использовать вложенные повторения, мы его не используем. Второй — счётчик, текущая итерация — индекс в нашей таблице. Третий — дополнительный параметр, мы в нем передаём полином контрольной суммы.
Как-же работает BOOST_PP_REPEAT, если в перпроцессоре нет ни циклов, ни рекурсии. Очень просто — определено 256 макросов с именами типа BOOST_PP_REPEAT_x, где х — номер итерации, которые вызывают друг друга по цепочке. В макросе BOOST_PP_REPEAT склеивается имя макроса этой цепочки из токена BOOST_PP_REPEAT_ и количества требуемых повторений. Это несколько упрощенное объяснение, в реальности там чуть сложнее, но основной принцип такой.