Применение директивы #error

21/11/2010 - 22:24
  
   «Минутку» - скажете вы. «Я трачу достаточно времени на исправление ошибок, а вы предлагаете мне использовать директиву, вызывающую дополнительные ошибки?» Именно так! Скомпилированный, но неверный код, еще хуже, чем нескомпилированный код с ошибками. Я нашел три  распространенных случая, в которых может возникнуть эта проблема и где #error может помочь. Читайте и смотрите, согласны вы со мной или нет.

Незаконченный код

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

#error *** Nigel - Function incomplete. Fix before using ***

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

Код, зависимый от компилятора

  Каждый раз, когда я пытаюсь написать переносимый код, мне приходится искать компромисс между эффективностью и переносимостью, а в мире встраиваемых систем побеждает эффективность. Однако, что произойдет, если через несколько лет я вдруг  повторно использую код, позабыв о том, что в нем задействованы особенности какого-то конкретного компилятора? Скорее всего, код придется отлаживать гораздо дольше, чем бы мне этого хотелось. 
   Применение директивы #error может избавить нас от таких досадных ситуаций. Рассмотрим для наглядности пару примеров.

Пример 1

   Некий код, использующий переменные с плавающей точкой, требует как минимум 12-ти разрядов, чтобы выдать правильные результаты. Поэтому переменные описаны как long double. Но ISO Си требует только чтобы long double имел 10 разрядов. Таким образом, на некоторых машинах long double число может не подойти для выполнения вычислений. Чтобы предусмотреть эту ситуацию, я бы добавил в свой код следующий фрагмент:

#include <float.h>
#if (LDBL_DIG < 12) 
#error *** long doubles need 12 digit resolution. Do not use this compiler! *** 
#endif

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

Пример 2

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

#include <limits.h>
#if (INT_MAX != 32767) 
#error *** This file only works with 16-bit int. Don`t use this compiler! *** 
#endif

   Препроцессор проверит значение константы INT_MAX, заданной в заголовочном файле limits.h, и если она не удовлетворяет условию, выдаст сообщение об ошибке. Конечно, можно было бы поместить эти ограничения внутрь комментария, однако где гарантия, что его кто-то прочтет? А вот сообщения об ошибках читают все! 

Условно-компилируемый код

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

#if defined OPT_1 
/* Do option_1 */ 
#else 
/* Do option_2 */ 
#endif 

   Этот код означает следующее: если и только если определен идентификатор ОРТ_1, компилируется option_1; иначе - option_2. Проблема этого кода заключается в том, что без подробного исследования программы, пользователь не знает, что ОРТ_1 является переключателем компилятора. Вместо этого неподготовленный пользователь просто скомпилирует код без определения ОРТ_1 и получит альтернативную реализацию, независимо от того требовалось это или нет. 
   Программист, осведомленный об этой проблеме, может написать код так:

#if defined OPT_1 
/* Do option 1 */ 
#elif defined OPT_2 
/* Do option 2*/ 
#endif

 В этом случае отсутствие определения идентификаторов ОРТ_1 или ОРТ_2 скорее всего приведет к неизвестной ошибке компилятора. А пользователь кода застрянет, пытаясь определить, что нужно сделать, чтобы скомпилировать модуль. 
  Рассмотрим третий фрагмент кода, в котором используется директива препроцессора  #error. 

#if defined OPT_1 
/* Do option_1 */ 
#elif defined OPT_2 
/* Do option_2 */ 
#else 
#error *** You must define one of OPT_1 or OPT_2 *** 
#endif 

   Теперь компиляция не удастся, если пользователь не определил ОРТ_1 или ОРТ_2. И он получит сообщение, о том, что нужно сделать, чтобы скомпилировать модуль. Если бы этот подход был принят повсеместно, я бы сберег много времени за те годы, что были потрачены на попытки использования чужого кода.

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

Jones, Nigel. "In Praise of the #error Directive" Embedded Systems Programming, September 1999. Вольный перевод ChipEnable.Ru

Comments   

# brawaga 2010-11-22 04:44
Воистину.
# foxit 2010-11-23 07:01
Спасибо за статью
# Pashgan 2010-11-23 21:55
да пожалуйста
# Neiver 2010-11-30 13:53
Всем-бы хороша директива #error, но вот только работает она в купе с конструкцией #if/#endif. Что существенно ограничивает ее область применения проверкой констант препроцессора.
Для проверки константных вырыжений язака Си(не препроцессора) она не подходит. Например такая конструкция не сработает:
Code:
struct A{
int a, b, c;
...
//много всего
};

#if sizeof(A) > EEPROM_SIZE
#error struct A does not feet into EEPROM
#endif

Препроцессор ничего не знает о sizeof и о размере структур.

Я в таких случаях пользуюсь StaticAssert.
Простейшая реализация на Си выглядит так:
Code:
#define CONCAT2(First, Second) (First ## Second)
#define CONCAT(First, Second) CONCAT2(First, Second)
#define STATIC_ASSERT(e xpr) typedef char CONCAT(static_a ssert_failed_at _line_, __LINE__) [(expr) ? 1 : -1]


Пользоваться им так:
Code:
STATIC_ASSERT(s izeof(A) <= EEPROM_SIZE); // читать как: утверждаем, что размер А

меньше или равен размеру EEPROM
Если условие истено, то всё компилируется.
Если - ложно, то происходит попытка создать typedef для массива отрицательной длины, что приводит к ошибке.
Никаких накладных расходов очень гибко, удобно и легко читаемо. Работает под любым стандартным Си компилятором.
# Глазастер 2011-02-24 20:33
Да !
поддерживает хороший стиль
и систематизирует код
(мой точно !) :lol:
# Den 2011-02-25 10:30
>>#if (INT_MAX != 33767)
А разве не 32767?
# Pashgan 2011-03-15 19:48
Да.. опечатался.
# Zebra 2011-03-12 08:56
Регулярно пользуюсь #error.

Однако тот же Nigel Jones в тесте 0x10 вопросов, указал что им пользуются только ботаны.
# Pashgan 2011-03-15 19:51
Да это стеб.

У вас недостаточно прав для комментирования.