Программирование ARM IAR: базовое использование препроцессора Fri, April 26 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

IAR: базовое использование препроцессора Печать
Добавил(а) microsin   

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

В этой статье (перевод [1, 2]) объясняется базовые вопросы использования препроцессора, включая макросы, которые выглядят как объекты и функции, директивы подключения, условную компиляцию, и в завершение будут рассмотрены две специальные директивы #error и #pragma. Во второй части статьи рассмотрены дополнительные вопросы использования препроцессора.

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

[Директива #include]

Наиболее прямолинейным указанием компилятору является директива препроцессора #include. Когда препроцессор встречает её, то он просто открывает указанный в директиве #include файл, и вставляет текст этого файла в то место, где указана директива #include. В результате получается, что как будто содержимое файла директивы записано в то место, где директива указана.

Директива #include может принимать 2 формы, например:

#include < systemfile.h >
#include "myfile.h"

Первая форма, с угловыми скобками, применяется для подключения "стандартных" (для библиотек C) заголовочных файлов наподобие stdio.h. Другая форма, с двойными скобками, предназначена для подключения Ваших собственных, относящихся к приложению, заголовочных файлов.

[Макросы]

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

В основном есть 2 типа макросов: макросы наподобие объектов (object-like macros) и макросы наподобие функций (function-like macros). Отличие состоит в том, что у макросов function-like есть параметры.

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

Для определения макроса может использоваться директива #define препроцессора. В следующем примере мы определим макрос NUMBER_OF_PLAYERS на константу "2". Это макрос типа object-like.

#define NUMBER_OF_PLAYERS 2
 
int current_score[NUMBER_OF_PLAYERS];

Ниже показан function-like макрос PRINT_PLAYER, который привязан к более сложному куску кода.

#define PRINT_PLAYER(no) printf("Player %d is named %s", no, names[no])

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

#define A_MACRO_THAT_DOES_SOMETHING_TEN_TIMES \
 for (i = 0; i < 10; ++i)\
 { \
    do_something(); \
 }

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

Если Вы хотите отменить определение макроса, то можете для этого использовать директиву #undef.

[Макросы наподобие объектов]

Подобные объекту макросы (object-like macros) могут использоваться для замены идентификатора в исходном коде некоторым заменяющим куском кода.

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

#define SQUARE_ROOT_OF_TWO 1.4142135623730950488016887
 
double convert_side_to_diagonal(double x)
{
   return x * SQUARE_ROOT_OF_TWO;
}
 
double convert_diagonal_to_side(double x)
{
   return x / SQUARE_ROOT_OF_TWO;
}

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

#define BLA_BLA );
 
int test(int x)
{
   printf("%d", x BLA_BLA
}

[Макросы наподобие функций]

Подобные функциям макросы (function-like macros) это такие макросы, которые принимают параметры. Когда Вы используете такой макрос, то это очень похоже на вызов функции. Макрос function-like будет выглядеть например так:

#define SEND(x) output_array[output_index++] = x

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

Так, если Вы напишете в коде:

SEND(10)

то компилятор увидит следующее:

output_array[output_index++] = 10

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

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

[Условная компиляция]

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

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

Директивы препроцессора #ifdef, #ifndef, #if, #elif и #else используются для управления содержимым исходного кода. Директивы #ifdef подключают следующую за ними секцию кода, если был определен его символ (#ifndef наоборот, подключает следующий за ним код, если символ не определен). Пример:

#ifdef ARM_BUILD
 __ARM_do_something();     //Тут будет код для ARM.
#else 
 generic_do_something();   //А это код для других процессоров.
#endif

Директива #if может обработать любой тип целочисленного и логического выражения, например:

#if (NUMBER_OF_PROCESSES > 1) && (LOCKING == TRUE)
 lock_process();
#endif

Директива #elif работает, как если бы были скомбинированы друг с другом директивы #else и #if. Это позволяет выстраивать цепочки условных блоков компиляции. Директива #endif заканчивает блок условной компиляции.

Директивы #if и #elif могут использовать специальный оператор defined для проверки, определен ли символ. Это полезно, если нужно провести сложную проверку, например:

#if defined(VERSION) && (VERSION > 2)
 ...
#endif

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

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

Мы еше вернемся к условной компиляции, чтобы обсудить, в каких случаях лучше использовать #if-ы вместо #ifdef, и наоборот.

[Защита от повторного подключения (include guards)]

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

Это так называемая защита от повторного подключения, include guard. Выглядит она следующим образом:

#ifndef MYHEADER_H
#define MYHEADER_H
 
/* Защищенная от повторного включения область кода. */
 
#endif

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

Примечание: некоторые компиляторы поддерживают "нестандартную" директиву #pragma once, которая делает то же самое. К сожалению, старые компиляторы IAR её не поддерживают.

[Директива ошибки]

Директива #error может использоваться для генерации сообщения об ошибке компилятора. Это полезно для проверки целостности проекта, например:

#if USE_COLORS && !HAVE_DISPLAY
 #error "Нельзя использовать цвета, если у Вас нет дисплея"
#endif

Если компилятор встретит директиву #error, то он выведет её сообщение и остановит компиляцию. Другие компиляторы наподобие GCC также поддерживают директивы #message и #warning, которые выдают сообщение и не останавливают компиляцию. Все эти директивы удобны для отладки операторов условной компиляции, когда Вы не можете понять, какая ветка условного оператора компиляции активна, а какая нет.

[Директива #pragma]

Другой директивой препроцессора является #pragma. Она позволяет программисту управлять поведением компилятора, и дает разработчикам компилятора возможность реализовать различные расширения языка C (например, отражающие особенности конкретной архитектуры процессора). Часто #pragma используется для управления оптимизацией блоков кода, или для управления выравниванием данных. Здесь директива #pragma не рассматривается, потому что мало относится к основной задаче препроцессора.

[Продвинутые варианты использования препроцессора]

Здесь будет глубже рассмотрено применение function-like макросов и решение возможных возникающих при этом проблем. Будут представлены операторы # и ## препроцессора, как они могут использоваться при определении макросов. Рассмотрим старый, так называемый трюк trick-ofthe-trade "do { ... } while(0)". Также обсудим, что лучше использовать в условной компиляции - #if или #ifdef.

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

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

#define TIMES_TWO(x) x * 2

Для простых случаев этот макрос будет исправно выполнять свою работу. Например, TIMES_TWO(4) будет развернут в выражение 4 * 2, что будет корректно вычислено как 8. С другой стороны, Вы можете ожидать, что TIMES_TWO(4 + 5) будет вычислено как 18, правильно? Но это не так, потому что макрос просто заменит "x" параметром так, как это было записано. В результате компилятор увидит выражение "4 + 5 * 2", что будет вычислено как 14.

Решение состоит в том, чтобы всегда окружать параметр в теле макроса круглыми скобками, например:

#define TIMES_TWO(x) (x) * 2

Теперь макрос будет работать именно так, как ожидалось.

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

#define PLUS1(x) (x) + 1

Здесь мы корректно окружили параметр макроса x круглыми скобками. Этот макрос будет в некоторых местах кода работать нормально; например, следующий код выведет на печать 11, как и ожидалось:

printf("%d\n", PLUS1(10));

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

printf("%d\n", 2 * PLUS1(10));

Так что здесь происходит? И снова, из-за того, что фактически препроцессор просто заменит тело макроса его куском кода, компилятор увидит следующее:

printf("%d\n", 2 * (10) + 1);

Очевидно, это совсем не то, что ожидалось, потому что умножение в выражении всегда осуществляется перед сложением. Решение проблемы состоит в том, чтобы окружить тело макроса круглыми скобками:

#define PLUS1(x) ((x) + 1)

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

printf("%d\n", 2 * ((10) + 1));

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

#define SQUARE(x) ((x) * (x))
 
printf("%d\n", SQUARE(++i));

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

printf("%d\n", ((++i) * (++i)));

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

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

Что же делать, если Вы не можете написать макрос, у которого каждый из параметров появляется в теле макроса только 1 раз? Самый прямой ответ на этот вопрос - не использовать function-like макросы, потому что все современные компиляторы C и C++ поддерживают так называемые встраиваемые функции, которые определяются с ключевым словом inline. Это позволяет решить задачу макроса без его побочных эффектов, связанных с параметром макроса. Дополнительно компилятору проще находить ошибки неправильного использования типов и сообщать о них, потому что с функции и их параметры имеют тип, в отличие от макросов.

[Специальные функции макросов]

Создание строки оператором #. Оператор "#" может использоваться в function-like макросах для преобразования параметра в строку. На первый взгляд тут все очевидно, но если Вы примените простой подход, и будете это применять непосредственно в макросе, то к сожалению будете удивлены.

Например:

#define NAIVE_STR(x) #x
 
puts(NAIVE_STR(10)); /* Этот код выведет на печать "10". */

Следующий пример будет работать не так, как Вы ожидали:

#define NAME Anders
 
printf("%s", NAIVE_STR(NAME)); /* Будет напечатано NAME. */

Этот последний пример напечатает NAME, но это не то NAME, которое было определено, что очевидно не то, что Вы хотели. Что тут можно сделать? К счастью, есть стандартное решение:

#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)

Идея такой причудливой конструкции состоит в том, что когда разворачивается STR(NAME), то это будет развернуто в STR_HELPER(NAME), и предварительно STR_HELPER разворачивается как все object-like макросы, и сначала NAME будет заменено на Anders. Когда запускается function-like макрос STR_HELPER то ему в качестве параметра будет передано Anders.

Объединение идентификаторов оператором ##. Оператор "##" используется в макросе препроцессора чтобы показать, что мы должны соединить друг с другом малые фрагменты в и в результате получить идентификатор большего размера или большее число, что бы это ни было.

Для примера предположим, что у нас есть набор переменных:

MinTime, MaxTime, TimeCount.
MinSpeed, MaxSpeed, SpeedCount.

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

#define NAIVE_AVERAGE(x) \
   (((Max##x) - (Min##x)) / (x##Count))

Это будет работать для типичных случаев использования:

NAIVE_AVERAGE(Time);

Такой вызов будет развернут в следующее выражение:

return (((MaxTime) - (MinTime)) / (TimeCount));

Однако, как мы уже видели с оператором "#", в следующем контексте макрос не будет работать, как ожидалось:

#define TIME Time
 
NAIVE_AVERAGE(TIME)

К сожалению, это будет развернуто в следующее выражение, и компилятор выдаст ошибку на отсутствие переменных MaxTIME, MinTIME, TIMECount:

return (((MaxTIME) - (MinTIME)) / (TIMECount));

Здесь решение будет такое же простое, как и в вышеописанном примере реализации макроса STR. Мы будем разворачивать макрос за 2 шага. Обычно Вы должны определить простой макрос для склеивания чего-нибудь следующим образом:

#define GLUE_HELPER(x, y) x##y
#define GLUE(x, y) GLUE_HELPER(x, y)

Теперь мы готовы к использованию макроса склейки GLUE в нашем макросе AVERAGE:

#define AVERAGE(x) \
   (((GLUE(Max,x)) - (GLUE(Min,x))) / (GLUE(x,Count)))

[Как сделать макрос наподобие оператора, трюк "do {} while(0)"]

Очень удобно реализовать макрос, чтобы он выглядел и работал ("look-and-feel") так же, как и обычный код C.

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

Чтобы поддержать look-and-feel, мы должны позволить пользователю вставить точку с запятой после function-like макроса, тогда его вызов будет выглядеть как обычный оператор. Препроцессор только заменит макрос куском кода, так что мы должны гарантировать, что это не приведет в результате к ошибочной программе из-за последующей после макроса точки с запятой.

Например:

void test()
{
   a_function(); /* Точка с запятой не является частью
   A_MACRO(); замены макроса. */
}

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

#define DO_ONE() a_function(1,2,3)

Но что произойдет, если макрос состоит из двух операторов, например из двух вызовов функций? Почему следующая реализация макроса будет плохой идеей?

#define DO_TWO() first_function(); second_function()

В простом контексте это будет работать как ожидалось, например:

DO_TWO();

Это будет развернуто так:

first_function(); second_function();

Но что будет в контексте, где ожидается один оператор вместо двух? Пример:

if (... тут что-то проверяется ...)
   DO_TWO();

К сожалению, это будет развернуто в следующий код:

if (... тут что-то проверяется ...)
   first_function();
second_function();

Проблема в том, что только "first_function" попадет в тело оператора "if". Функция "second_function" не будет частью оператора проверки, так что она будет вызываться всегда.

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

#define DO_TWO() \
{ first_function(); second_function(); }

К сожалению, это все еще не будет работать так, как надо, если макрос используется в контексте операторов "if" и "else". Рассмотрим следующий пример:

if (... тут что-то проверяется ...)
   DO_TWO();
else
   ...

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

if (... тут что-то проверяется ...)
   { first_function(); second_function(); };
else
   ...

Тело "if" состоит из 2 операторов: это будут составной оператор внутри фигурных скобок (первый оператор), и пустой оператор, который состоит только из точки с запятой (второй оператор). Это недопустимо для языка C, поскольку между "if (...)" и "else" должен быть только один оператор!

Есть старый трюк для решение этой проблемы:

#define DO_TWO() \
do { first_function(); second_function(); } while(0)

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

В этом случае нам просто повезло. Цикл выполнится только 1 раз, потому что для продолжения цикла его проверяемое выражение while должно быть true. У нас это не так, while(0) гарантирует, что условие продолжения цикла не соблюдается, и тело цикла будет выполнено только 1 раз.

Эта версия макроса выглядит и работает точно так же, как и нормальная функция (принцип look-and-feel выполняется). Когда макрос используется внутри "if ... else", то это будет развернуто в корректный код C:

if (... тут что-то проверяется ...)
   do { first_function(); second_function(); } while(0);
else
   ...

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

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

[Почему лучше использовать #if вместо #ifdef]

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

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

Использование #ifdef. Если применяются #ifdef-ы, то код будет выглядеть примерно так:

#ifdef MY_COOL_FEATURE ... подключено, если используется "моя крутая фича" ...
#endif
#ifndef MY_COOL_FEATURE ... исключаемый код, если используется "моя крутая фича" ...
#endif

Приложение, которое использует #ifdef-ы, обычно не имеет специальной возможности обрабатывать значения конфигурационных переменных.

Использование #if. Когда Вы используете #if-ы, то обрабатываемые ими символы препроцессора всегда определены, просто могут иметь разные значения. Символы, которые соответствуют операции директивы #ifdef, могут быть либо в значении true, либо false, которые должны быть представлены целыми числами 1 и 0 соответственно.

#if MY_COOL_FEATURE ... подключено, если используется "моя крутая фича" ...
#endif
#if !MY_COOL_FEATURE ... исключаемый код, если используется "моя крутая фича" ...
#endif

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

#if INTERFACE_VERSION == 0
 printf("Hello\n");
#elif INTERFACE_VERSION == 1
 print_in_color("Hello\n", RED);
#elif INTERFACE_VERSION == 2
 open_box("Hello\n");
#else
 #error "Неизвестная версия интерфейса INTERFACE_VERSION"
#endif

Приложение, которое использует этот стиль, обычно нуждается в определении по умолчанию для своих конфигурационных переменных. Это должно быть сделано, к примеру, в файле "defaults.h". Когда конфигурируется приложение, то некоторые символы должны быть заданы либо через командную строку компилятора, либо (что возможно более удобно) в специальном конфигурационном файле, скажем "config.h". Этот конфигурационный заголовочный файл должен быть пустой, если должна использоваться конфигурация по умолчанию.

Пример содержимого файла заголовка defaults.h:

#include "config.h"
/*
 * MY_COOL_FEATURE == true, если должна использоваться "моя крутая фича".
 */
#ifndef MY_COOL_FEATURE
 #define MY_COOL_FEATURE 0 /* По умолчанию "крутая фича" отключена. */
#endif

#if против #ifdef: что из этого следует использовать? Пока что оба метода кажутся эквивалентными. Если посмотреть на реальный мир приложений, то можно увидеть, что активно используются оба метода. На первый взгляд #ifdef-ы выглядят проще, но мой опыт показывает, что #if-ы лучше в дальней перспективе использования.

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

Например, следующая опечатка останется незамеченной в процессе компиляции:

#ifdef MY_COOL_FUTURE /* на самом деле должно быть "FEATURE". */
... Тут какой-то важный код ...
#endif

Другими словами, большинство компиляторов могут определить, что в директиве #if использован неопределенный символ. Стандарт C говорит, что это должно быть возможным, и в этом случае символ должен считаться с нулевым значением. В случае компилятора IAR Systems будет выдано диагностическое сообщение Pe193; по умолчанию это сообщение типа remark, но его значение может быть повышено до предупреждения (warning) или, что еще лучше, до ошибки (error).

2. Директива #ifdef не является безопасной для использования в будущем. Предположим, что Ваше приложение сконфигурировано с использованием #ifdef-ов. Что произойдет, если Вы захотите гарантировать, что свойство будет сконфигурировано неким особым образом - например, если Вы должны поддерживать цвета — даже если значение по умолчанию должно поменяться в будущем? К сожалению, с помощью #ifdef это реализовать невозможно.

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

3. Директива #ifdef диктует правила для значения по умолчанию. Если для конфигурации приложения используются #ifdef-ы, то конфигурация по умолчанию не должна задавать какие-либо дополнительные символы. Для подключения дополнительных функций существует одно прямолинейное решение - нужно просто определить MY_COOL_FEATURE, и точка. Однако, когда функция должна быть удалена, имя идентификатора часто становится DONT_USE_COLORS.

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

#ifndef DONT_USE_COLORS
 ... какие-то действия ...
#endif

При использовании #ifndef иногда не бывает других вариантов, и приходится писать только так. Это возможно похоже на лишние придирки, но если Вы встретитесь с анализом больших объемов кода с подобными конструкциями, то сломаете себе мозг. Лучше всего написать примерно так:

#if USE_COLORS
 ... какие-то действия ...
#endif

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

6. Почти невозможно поменять значение по умолчанию для директив #ifdef. Однако, самый большой недостаток использования #ifdef-ов в том, что приложение не может поменять значение по умолчанию без изменения всех #ifdef-ов во всем приложении.

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

[Миграция от #ifdef к #if]

Вы скажете: да, все это звучит красиво, но сейчас у нас в приложении везде используются #ifdef-ы, и нам скорее всего придется мириться с этим.

На это можно возразить, что так делать не нужно! Миграция с #ifdef-ов на #if-ы более-менее тривиальна. Дополнительно Вы могли бы обеспечить обратную совместимость для своих старых переменных конфигурации.

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

Если Вы сохранили имя конфигурационной переменной, и пользователь определил его пустым (как в "#define MY_COOL_FEATURE"), то он получит ошибку компиляции на первом #if, который использует этот символ. Сообщение об ошибке (директива #error) должна подсказать пользователю, какие изменения следует произвести, чтобы определить корректную конфигурацию.

Создайте файл заголовка defaults.h, как было описано выше, и гарантируйте, что его подключают все исходные файлы (если так не сделать, то ошибка сразу станет заметной после того, как будет получена ошибка при проверке в том месте исходного кода, где конфигурационная переменная не определена). А начале файла заголовка Вы должны привязать старые имена директив #ifdef к новым, например:

/* Старые конфигурационные переменные, гарантия что они все еще работают. */
#ifdef DONT_USE_COLORS
 #define USE_COLORS 0
#endif
/* Установка значения по умолчанию. */
#ifndef USE_COLORS
 #define USE_COLORS 1
#endif

Затем переименуйте все #ifdef-ы в #if-ы во всех исходных файлах следующим образом:

Было так Надо сделать теперь так
#ifdef MY_COOL_FEATURE #if MY_COOL_FEATURE 
#ifndef MY_COOL_FEATURE #if !MY_COOL_FEATURE
#ifdef DONT_USE_COLORS #if !USE_COLORS
#ifndef DONT_USE_COLORS #if USE_COLORS

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

[Ссылки]

1. Basics of using the preprocessor site:iar.com.
2. Advanced preprocessor tips and tricks site:iar.com.
3Переменные аргумента IAR ($PROJ_DIR$, $DATE$, и т. п.).

 

Комментарии  

 
0 #1 Георгий 23.04.2019 18:22
Спасибо за информацию, периодически перечитываю, очень помогает погрузиться в тонкости.
Цитировать
 

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


Защитный код
Обновить

Top of Page