Поиск…


Вступление

Препроцессор C представляет собой простой текстовый парсер / заменитель, который запускается до фактической компиляции кода. Используется для расширения и облегчения использования языка C (и более поздней версии C ++), его можно использовать для:

а. Включение других файлов с помощью #include

б. Определите макрос замены текста с помощью #define

с. Условная компиляция с использованием #if #ifdef

д. Спецификация платформы / компилятора (как расширение условной компиляции)

замечания

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

Включить охранников

Заголовочный файл может быть включен в другие файлы заголовков. Исходный файл (блок компиляции), который включает несколько заголовков, может поэтому косвенно включать некоторые заголовки более одного раза. Если такой заголовочный файл, который включен более одного раза, содержит определения, компилятор (после предварительной обработки) обнаруживает нарушение правила Единого определения (например, §3.2 стандарта 2003 C ++) и поэтому выдает сбой диагностики и компиляции.

Множественное включение предотвращается с помощью «include guard», которые иногда также называются защитами заголовков или защитой от макросов. Они реализованы с использованием препроцессора #define , #ifndef , директив #endif .

например

// Foo.h
#ifndef FOO_H_INCLUDED 
#define FOO_H_INCLUDED

class Foo    //  a class definition
{
};

#endif

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

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

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

Большинство реализаций на C ++ также поддерживают директиву #pragma once которая гарантирует, что файл включен только один раз в рамках одной компиляции. Это стандартная директива de facto , но она не является частью стандарта ISO C ++. Например:

// Foo.h
#pragma once

class Foo
{
};

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

Ряд руководств по кодированию и стандартов достоверности для C ++ специально препятствует использованию препроцессора, отличному от #include заголовочных файлов, или для целей размещения включают в себя защитные устройства в заголовках.

Условная логика и кросс-платформенная обработка

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

Три видных варианта использования:

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

Пример a: кросс-платформенный подход для удаления файлов (иллюстративный):

#ifdef _WIN32
#include <windows.h> // and other windows system files
#endif
#include <cstdio>

bool remove_file(const std::string &path) 
{
#ifdef _WIN32
  return DeleteFile(path.c_str());
#elif defined(_POSIX_VERSION) || defined(__unix__)
  return (0 == remove(path.c_str()));
#elif defined(__APPLE__)
  //TODO: check if NSAPI has a more specific function with permission dialog
  return (0 == remove(path.c_str()));
#else 
#error "This platform is not supported"
#endif
}

Макросы, такие как _WIN32 , __APPLE__ или __unix__ , обычно предопределены соответствующими реализациями.

Пример b: Включение дополнительного ведения журнала для сборки отладки:

void s_PrintAppStateOnUserPrompt()
{
    std::cout << "--------BEGIN-DUMP---------------\n"
              << AppState::Instance()->Settings().ToString() << "\n"
#if ( 1 == TESTING_MODE ) //privacy: we want user details only when testing
              << ListToString(AppState::UndoStack()->GetActionNames())
              << AppState::Instance()->CrntDocument().Name() 
              << AppState::Instance()->CrntDocument().SignatureSHA() << "\n"
#endif
              << "--------END-DUMP---------------\n"
}

Пример c: Включите премиум-функцию в отдельную сборку продукта (обратите внимание: это иллюстративно. Часто лучше дать возможность разблокировать функцию без необходимости переустановки приложения)

void MainWindow::OnProcessButtonClick()
{
#ifndef _PREMIUM
    CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website");
    return;
#endif
    //...actual feature logic here
}

Некоторые общие трюки:

Определение символов во время вызова:

Препроцессор можно вызывать с предопределенными символами (с необязательной инициализацией). Например, эта команда ( gcc -E запускает только препроцессор)

gcc -E -DOPTIMISE_FOR_OS_X -DTESTING_MODE=1 Sample.cpp

процессов Sample.cpp так же, как если бы #define OPTIMISE_FOR_OS_X и #define TESTING_MODE 1 были добавлены в начало Sample.cpp.

Определение макроса:

Если макрос не определен и его значение сравнивается или проверяется, препроцессор почти всегда молча принимает значение 0 . Есть несколько способов работать с этим. Один из подходов состоит в том, чтобы предположить, что параметры по умолчанию представлены как 0, и любые изменения (например, профиль профиля приложения) должны быть явно выполнены (например, ENABLE_EXTRA_DEBUGGING = 0 по умолчанию, установите -DENABLE_EXTRA_DEBUGGING = 1 для переопределения). Другой подход - сделать все определения и значения по умолчанию явными. Этого можно достичь с помощью комбинации директив #ifndef и #error :

#ifndef (ENABLE_EXTRA_DEBUGGING)
// please include DefaultDefines.h if not already included.
#    error "ENABLE_EXTRA_DEBUGGING is not defined"
#else
#    if ( 1 == ENABLE_EXTRA_DEBUGGING )
  //code
#    endif
#endif

макрос

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

// This is an object-like macro
#define    PI         3.14159265358979

// This is a function-like macro.
// Note that we can use previously defined macros
// in other macro definitions (object-like or function-like)
// But watch out, its quite useful if you know what you're doing, but the
// Compiler doesnt know which type to handle, so using inline functions instead
// is quite recommended (But e.g. for Minimum/Maximum functions it is quite useful)
#define    AREA(r)    (PI*(r)*(r))

// They can be used like this:
double pi_macro   = PI;
double area_macro = AREA(4.6);

Библиотека Qt использует эту технику для создания системы метаобъектов путем объявления пользователем макроса Q_OBJECT во главе пользовательского класса, расширяющего QObject.

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


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

double pi_squared = PI * PI;
// Compiler sees:
double pi_squared = 3.14159265358979 * 3.14159265358979;

double area = AREA(5);
// Compiler sees:
double area = (3.14159265358979*(5)*(5))

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

#define BAD_AREA(r) PI * r * r

double bad_area = BAD_AREA(5 + 1.6);
// Compiler sees:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;

double good_area = AREA(5 + 1.6);
// Compiler sees:
double good_area = (3.14159265358979*(5 + 1.6)*(5 + 1.6));

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

int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));

Кроме того, макросы не обеспечивают безопасности типов, что приводит к трудно понятным ошибкам в отношении несоответствия типов.


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

#define IF_BREAKER(Func) Func();

if (some_condition)
    // Oops.
    IF_BREAKER(some_func);
else
    std::cout << "I am accidentally an orphan." << std::endl;

В этом примере непреднамеренная двойная точка с запятой разбивает блок if...else , не позволяя компилятору сопоставить else с if . Чтобы предотвратить это, точка с запятой не указана в определении макроса, что приведет к «проглатыванию» точки с запятой сразу после ее использования.

#define IF_FIXER(Func) Func()

if (some_condition)
    IF_FIXER(some_func);
else
    std::cout << "Hooray!  I work again!" << std::endl;

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

#define DO_SOMETHING(Func, Param) Func(Param, 2)

// ...

some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));

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

#define TEXT "I \
am \
many \
lines."

// ...

std::cout << TEXT << std::endl; // Output:   I am many lines.

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

#define CREATE_OUTPUT_AND_DELETE(Str) \
    std::string* tmp = new std::string(Str); \
    std::cout << *tmp << std::endl; \
    delete tmp;

// ...

CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")

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

#define DO_STUFF(Type, Param, ReturnVar) do { \
    Type temp(some_setup_values); \
    ReturnVar = temp.process(Param); \
} while (0)

int x;
DO_STUFF(MyClass, 41153.7, x);

// Compiler sees:

int x;
do {
    MyClass temp(some_setup_values);
    x = temp.process(41153.7);
} while (0);

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

#define VARIADIC(Param, ...) Param(__VA_ARGS__)

VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);

Обратите внимание, что во время расширения __VA_ARGS__ может быть помещен в любом месте определения и будет правильно __VA_ARGS__ .

#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)

VARIADIC2(some_func, 3, 8, 6, 9);
// Compiler sees:
some_func(8, 6, 9, 3);

В случае вариационного параметра с нулевым аргументом разные компиляторы будут обрабатывать конечную запятую по-разному. Некоторые компиляторы, такие как Visual Studio, беззвучно проглатывают запятую без специального синтаксиса. Другие компиляторы, такие как GCC, требуют, чтобы вы разместили ## непосредственно перед __VA_ARGS__ . Из-за этого разумно условно определять переменные макросы, когда переносимость вызывает беспокойство.

// In this example, COMPILER is a user-defined macro specifying the compiler being used.

#if       COMPILER == "VS"
    #define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif     COMPILER == "GCC"
    #define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */

Сообщения об ошибках препроцессора

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

например, Return Error, если версия gcc 3.0.0 или более ранняя.

#if __GNUC__ < 3
#error "This code requires gcc > 3.0.0"
#endif

например, «Ошибка возврата» при компиляции на компьютере Apple.

#ifdef __APPLE__
#error "Apple products are not supported in this release"
#endif

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

Предопределенные макросы - это те, которые определяет компилятор (в отличие от тех, которые пользователь определяет в исходном файле). Эти макросы не должны переопределяться или неопределяться пользователем.

Следующие макросы предопределены стандартом C ++:

  • __LINE__ содержит номер строки строки, на которой используется этот макрос, и может быть изменен директивой #line .
  • __FILE__ содержит имя файла, в котором этот макрос используется, и может быть изменен директивой #line .
  • __DATE__ содержит дату (в формате "Mmm dd yyyy" ) компиляции файла, где Mmm отформатирован так, как если бы был получен вызов std::asctime() .
  • __TIME__ содержит время (в формате "hh:mm:ss" ) компиляции файла.
  • __cplusplus определяется (совместимыми) компиляторами C ++ при компиляции файлов C ++. Его значение - стандартная версия, которая полностью соответствует компилятору, то есть 199711L для C ++ 98 и C ++ 03, 201103L для C ++ 11 и 201402L для C ++ 14.
C ++ 11
  • __STDC_HOSTED__ определяется как 1 если реализация размещена , или 0 если она является автономной .
C ++ 17
  • __STDCPP_DEFAULT_NEW_ALIGNMENT__ содержит литерал size_t , который является выравниванием, используемым для вызова operator new выравнивания-незнания operator new .

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

  • __STDC__ имеет значение, зависящее от реализации, и обычно определяется только при компиляции файла как C, чтобы обозначить полное соответствие стандарту C. (Или никогда, если компилятор решает не поддерживать этот макрос.)
C ++ 11
  • __STDC_VERSION__ имеет значение, зависящее от реализации, и его значение обычно является версией C, аналогично тому, как __cplusplus является __cplusplus на C ++. (Или даже не определено, если компилятор решает не поддерживать этот макрос.)
  • __STDC_MB_MIGHT_NEQ_WC__ определяется как 1 , если значения узкой кодировки базового набора символов могут быть не равны значениям их широких копий (например, если (uintmax_t)'x' != (uintmax_t)L'x' )
  • __STDC_ISO_10646__ определяется, если wchar_t закодирован как Unicode и расширяется до целочисленной константы в форме yyyymmL , указывая, что поддерживается последняя версия Unicode.
  • __STDCPP_STRICT_POINTER_SAFETY__ определяется как 1 , если реализация имеет строгую безопасность указателя (в противном случае она ослабла безопасность указателя )
  • __STDCPP_THREADS__ определяется как 1 , если программа может иметь более одного потока выполнения (применимая к автономной реализации - реализация хоста всегда может иметь более одного потока)

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

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

Некоторые макросы предназначены только для поддержки некоторой функции:

#ifdef __cplusplus // if compiled by C++ compiler
extern "C"{ // C code has to be decorated
   // C library header declarations here
}
#endif

Другие очень полезны для отладки:

C ++ 11
bool success = doSomething( /*some arguments*/ );
if( !success ){
    std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2
              << " in function " << __func__ << "()"
              << " in file " << __FILE__
              << std::endl;
}

И другие для тривиального контроля версий:

int main( int argc, char *argv[] ){
    if( argc == 2 && std::string( argv[1] ) == "-v" ){
        std::cout << "Hello World program\n"
                  << "v 1.1\n" // I have to remember to update this manually
                  << "compiled: " << __DATE__ << ' ' << __TIME__ // this updates automagically
                  << std::endl;
    }
    else{
        std::cout << "Hello World!\n";
    }
}

X-макросы

Идиоматический метод генерации повторяющихся структур кода во время компиляции.

X-macro состоит из двух частей: списка и выполнения списка.

Пример:

#define LIST \
    X(dog)   \
    X(cat)   \
    X(racoon)

// class Animal {
//  public:
//    void say();
// };

#define X(name) Animal name;
LIST
#undef X

int main() {
#define X(name) name.say();
    LIST
#undef X

    return 0;
}

который расширяется препроцессором до следующего:

Animal dog;
Animal cat;
Animal racoon;

int main() {
    dog.say();
    cat.say();
    racoon.say();

    return 0;
}    

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

Источник: https://en.wikipedia.org/wiki/X_Macro

См. Также: X-макросы


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

#define LIST(MACRO) \
    MACRO(dog) \
    MACRO(cat) \
    MACRO(racoon)

Теперь вы явно указываете, какой макрос следует использовать при расширении списка, например

#define FORWARD_DECLARE_ANIMAL(name) Animal name;
LIST(FORWARD_DECLARE_ANIMAL)

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

//a walkaround for Visual studio
#define EXPAND(x) x

#define LIST(MACRO, ...) \
    EXPAND(MACRO(dog, __VA_ARGS__)) \
    EXPAND(MACRO(cat, __VA_ARGS__)) \
    EXPAND(MACRO(racoon, __VA_ARGS__))

Первый аргумент предоставляется LIST , а остальное предоставляется пользователем в вызове LIST . Например:

#define FORWARD_DECLARE(name, type, prefix) type prefix##name;
LIST(FORWARD_DECLARE,Animal,anim_)
LIST(FORWARD_DECLARE,Object,obj_)

будет расширяться до

Animal anim_dog;
Animal anim_cat;
Animal anim_racoon;
Object obj_dog;
Object obj_cat;
Object obj_racoon;        

# прагма один раз

Большинство, но не все, реализации C ++ поддерживают директиву #pragma once которая гарантирует, что файл включен только один раз в рамках одной компиляции. Он не является частью стандарта ISO C ++. Например:

// Foo.h
#pragma once

class Foo
{
};

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

С некоторыми компиляторами, особенно с теми, которые используют предварительно скомпилированные заголовки, #pragma once может привести к значительному ускорению процесса компиляции. Аналогичным образом, некоторые препроцессоры добиваются ускорения компиляции, отслеживая используемые заголовки, включая защитные устройства. Чистая выгода, когда используются как # #pragma once и включенные охранники, зависит от реализации и может быть либо увеличением, либо уменьшением времени компиляции.

#pragma once объединенный с включенными охранниками, был рекомендуемым макетом для файлов заголовков при написании приложений на основе MFC в Windows и был сгенерирован add class add dialog Visual Studio, add dialog , add windows мастеров. Поэтому очень часто их объединяют в C ++ Windows Applicants.

Операторы препроцессора

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

// preprocessor will convert the parameter x to the string literal x
#define PRINT(x) printf(#x "\n")

PRINT(This line will be converted to string by preprocessor);
// Compiler sees
printf("This line will be converted to string by preprocessor""\n");

Компилятор объединяет две строки, а окончательный аргумент printf() будет строковым литералом с символом новой строки в конце.

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

PRINT(   This line will be converted to string by preprocessor );

Если для параметра строкового литерала требуется escape-последовательность, как перед двойной кавычкой (), она будет автоматически вставлена ​​препроцессором.

PRINT(This "line" will be converted to "string" by preprocessor); 
// Compiler sees
printf("This \"line\" will be converted to \"string\" by preprocessor""\n");

Оператор ## или оператор сопоставления токенов используется для конкатенации двух параметров или жетонов макроса.

// preprocessor will combine the variable and the x
#define PRINT(x) printf("variable" #x " = %d", variable##x)

int variableY = 15;
PRINT(Y);
//compiler sees
printf("variable""Y"" = %d", variableY);

и конечный результат будет

variableY = 15


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow