Введение
Препроцессор макросов для нескольких языков программирования
Препроцессор C — это макропрепроцессор для ряда языков программирования, таких как C, Objective-C, C++ и различные диалекты Fortran. Препроцессор обеспечивает включение файлов заголовков, раскрытие макросов, условную компиляцию и управление номерами строк. Язык предпроцессорных директив лишь слабо связан с грамматикой языка C, поэтому он иногда используется для обработки других типов текстовых файлов.
История
Предпроцессор был внедрён в язык C примерно в 1973 году по настоянию Алана Снайдера, а также с учётом полезности механизмов включения файлов, реализованных в BCPL и PL/I. Его первоначальная версия предоставляла лишь включение файлов и простую замену строк с помощью директив #include и #define для макросов без параметров соответственно. Вскоре после этого он был расширен, сначала Майком Леском, а затем Джоном Райзером, для поддержки макросов с аргументами и условной компиляции. Предпроцессор C являлся частью давней традиции разработки макроязыков в Bell Labs, начатой Дугласом Иствудом и Дугласом Макилроем в 1959 году.
Фазы
Предварительная обработка определяется первыми четырьмя (из восьми) фазами трансляции, указанными в стандарте C. Замена триграфов: препроцессор заменяет последовательности триграфов соответствующими символами. Эта фаза будет удалена в C23, следуя примеру C++17. Склейка строк: физические строки исходного кода, продолжающиеся с помощью экранированных символов новой строки, объединяются в логические строки. Токенизация: препроцессор разбивает полученный результат на препроцессорные токены и пробелы, заменяя комментарии пробелами. Расширение макросов и обработка директив: выполняются строки директив препроцессора, включая включение файлов и условную компиляцию. Препроцессор одновременно расширяет макросы и, начиная с версии стандарта C 1999 года, обрабатывает операторы #pragma.
Включение бинарных ресурсов
C23 представит директиву #embed для включения бинарных ресурсов. Это позволит включать в программу бинарные файлы (например, изображения), не являющиеся допустимыми исходными файлами C (например, XBM), без необходимости обработки внешними инструментами, такими как xxd, и без использования строковых литералов, которые имеют ограничение по длине в MSVC. Подобно xxd, директива заменяется списком целых чисел, разделенных запятыми, соответствующих данным указанного ресурса. Более точно, если массив типа инициализируется с помощью директивы #embed, результат будет таким же, как если бы ресурс был записан в массив с помощью fread (если только параметр не изменит ширину внедряемого элемента на значение, отличное от CHAR_BIT). Помимо удобства, #embed также проще обрабатывать компиляторам, поскольку им разрешено пропускать развертывание директивы в полную форму согласно правилу "as if". Файл для внедрения можно указать аналогично #include, то есть либо между угловых скобок, либо в двойных кавычках. Директива также позволяет передавать ей определенные параметры для настройки ее поведения, которые следуют за именем файла. Стандарт C определяет следующие параметры, а реализации могут определять свои собственные. Параметр limit используется для ограничения ширины включаемых данных. Он предназначен в основном для использования с "бесконечными" файлами, такими как urandom. Параметры prefix и suffix позволяют программисту указать префикс и суффикс для внедряемых данных, которые используются только в том случае, если внедряемый ресурс не пуст. Наконец, параметр if empty заменяет всю директиву, если ресурс пуст (что происходит, если файл пуст или указан предел 0). Все стандартные параметры также могут быть заключены в двойные подчеркивания, как и стандартные атрибуты в C23, например, prefix взаимозаменяем с __prefix__. Определяемые реализацией параметры используют форму, аналогичную синтаксису атрибутов (например, vendor::attr), но без квадратных скобок. Хотя для всех стандартных параметров требуется передавать аргумент (например, limit требует указания ширины), это обычно необязательно, и даже круглые скобки можно опустить, если аргумент не требуется, что может быть в случае некоторых параметров, определенных реализацией.
Реализация
Все реализации C, C++ и Objective-C предоставляют препроцессор, так как предварительная обработка является обязательным этапом для этих языков, а его поведение описано в официальных стандартах для них, например, в стандарте ISO C. Реализации могут иметь собственные расширения и отклонения, а также различаться по степени соответствия письменным стандартам. Их конкретное поведение может зависеть от флагов командной строки, передаваемых при запуске. Например, для повышения соответствия стандартам препроцессор GNU C может быть запущен с определенными флагами.
Специфические для компилятора функции препроцессора
Директива #pragma — это директива, специфичная для компилятора, которую производители компиляторов могут использовать в своих целях. Например, #pragma часто используется для подавления определенных сообщений об ошибках, управления отладкой кучи и стека и тому подобное. Компилятор с поддержкой библиотеки параллелизации OpenMP может автоматически параллелизировать цикл for с помощью #pragma omp parallel for. C99 представил несколько стандартных директив #pragma, имеющих вид #pragma STDC, которые используются для управления реализацией чисел с плавающей точкой. Также была добавлена альтернативная форма, похожая на макрос. Многие реализации не поддерживают триграфы или не заменяют их по умолчанию. Многие реализации (такие как компиляторы C от GNU, Intel, Microsoft и IBM) предоставляют нестандартную директиву для вывода предупреждающего сообщения в выходные данные, но не останавливают процесс компиляции (C23 и C++23 добавят в стандарт для этой цели). Типичное применение — предупреждение об использовании устаревшего кода, который теперь не рекомендуется к использованию и включен только для обеспечения совместимости; например: // GNU, Intel и IBM #warning "Не используйте ABC, который устарел. Используйте XYZ вместо этого." // Microsoft #pragma message("Не используйте ABC, который устарел. Используйте XYZ вместо этого.") Некоторые препроцессоры Unix традиционно предоставляли "утверждения", которые мало похожи на утверждения, используемые в программировании. GCC предоставляет #include next для объединения заголовочных файлов с одинаковым именем.
#warning "Do not use ABC, which is deprecated. Use XYZ instead. "// Microsoft
#pragma message("Do not use ABC, which is deprecated. Use XYZ instead.") Some Unix preprocessors traditionally provided "assertions," which have little similarity to assertions used in programming. GCC provides #include next for chaining headers of the same name.
Языковые особенности препроцессора
Существуют некоторые директивы препроцессора, которые были добавлены в препроцессор C спецификациями определенных языков и специфичны для этих языков. В препроцессорах Objective-C есть #import, который аналогичен #include, но включает файл только один раз. Распространенная прагма поставщика с похожей функциональностью в C — #pragma once. В C++ начиная с C++20 появились директивы import и module для модулей. Эти директивы — единственные, которые не начинаются с символа #; вместо этого они начинаются со слов import и module соответственно, которым может необязательно предшествовать export.
Другие применения
Поскольку препроцессор C может быть вызван отдельно от компилятора, с которым он поставляется, его можно использовать независимо для различных языков. Заметными примерами являются его применение в устаревшей системе imake и для предварительной обработки Fortran. Однако такое использование в качестве универсального препроцессора ограничено: входной язык должен быть достаточно близок к C. Intel предлагает препроцессор Fortran, fpp, для использования с компилятором ifort, обладающий схожими возможностями. CPP также приемлемо работает с большинством языков ассемблера и языков, подобных Algol. Это требует, чтобы синтаксис языка не конфликтовал с синтаксисом CPP, то есть не было строк, начинающихся с #, и чтобы двойные кавычки, которые cpp интерпретирует как строковые литералы и, следовательно, игнорирует, не имели иного синтаксического значения. "Традиционный режим" (работающий как препроцессор до ISO C) обычно более лоялен и лучше подходит для подобных задач. Препроцессор C не является машиной Тьюринга, но очень близок к этому: можно задавать рекурсивные вычисления, но с фиксированным верхним пределом глубины рекурсии. Однако препроцессор C не предназначен для использования в качестве, и не работает как, язык программирования общего назначения. Поскольку препроцессор C не обладает некоторыми функциями других препроцессоров, такими как рекурсивные макросы, выборочное раскрытие в зависимости от кавычек и вычисление строк в условных выражениях, он значительно уступает более универсальным макропроцессорам, таким как m4.