Почему следует избегать #ifdef в файлах .c? - PullRequest
31 голосов
/ 05 декабря 2009

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

Ответы [ 11 ]

55 голосов
/ 05 декабря 2009

Трудно поддерживать. Лучше использовать интерфейсы для абстракции кода, специфичного для платформы, чем злоупотреблять условной компиляцией, разбрасывая #ifdef s по всей вашей реализации.

Е.Г.

void foo() {
#ifdef WIN32
   // do Windows stuff
#else
   // do Posix stuff
#endif
   // do general stuff
}

Не приятно. Вместо этого есть файлы foo_w32.c и foo_psx.c с

foo_w32.c:

void foo() {
    // windows implementation
}

foo_psx.c:

void foo() {
    // posix implementation
}

foo.h:

void foo();  // common interface

Затем есть 2 make-файла 1 : Makefile.win, Makefile.psx, каждый из которых компилирует соответствующий файл .c и ссылается на нужный объект.

Незначительная поправка:

Если реализация foo() зависит от некоторого кода, который появляется на всех платформах, например, common_stuff() 2 , просто вызовите это в ваших foo() реализациях.

1036 * Е.Г. *

common.h:

void common_stuff();  // May be implemented in common.c, or maybe has multiple
                      // implementations in common_{A, B, ...} for platforms
                      // { A, B, ... }. Irrelevant.

foo_ {w32, psx} .c:

void foo()  {  // Win32/Posix implementation
   // Stuff
   ...
   if (bar) {
     common_stuff();
   }
}

Хотя вы, возможно, повторяете вызов функции для common_stuff(), вы не можете параметризовать ваше определение foo() для платформы, если оно не следует очень специфическому шаблону. Как правило, различия платформ требуют совершенно разных реализаций и не следуют таким шаблонам.


  1. Makefiles используются здесь для иллюстрации. Ваша система сборки может вообще не использовать make, например, если вы используете Visual Studio, CMake, Scons и т. Д.
  2. Даже если common_stuff() на самом деле имеет несколько реализаций, различающихся для разных платформ.
6 голосов
/ 05 декабря 2009

(Отчасти заданный вопрос)

Однажды я увидел совет, предлагающий использовать блоки #if(n)def/#endif для использования при отладке / изоляции кода вместо комментариев.

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

/* <-- begin debug cmnt   if (condition) /* comment */
/* <-- restart debug cmnt {
                              ....
                          }
*/ <-- end debug cmnt

Вместо этого это будет:

#ifdef IS_DEBUGGED_SECTION_X

                if (condition) /* comment */
                {
                    ....
                }
#endif

Мне показалось, что это хорошая идея. Жаль, что я не мог вспомнить источник, чтобы я мог связать его: (

3 голосов
/ 05 декабря 2009

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

Это скорее руководство, чем строгое правило, ИМХО. Но я согласен с тем, что решения на основе c-синтаксиса предпочтительнее магии препроцессора.

3 голосов
/ 05 декабря 2009
  1. Потому что тогда, когда вы делаете результаты поиска, вы не знаете, входит или нет код, не читая его.

  2. Поскольку они должны использоваться для зависимостей ОС / Платформы, и, следовательно, такой код должен находиться в файлах, таких как io_win.c или io_macos.c

2 голосов
/ 05 декабря 2009

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

Однажды я потратил неделю на отладку многопоточного приложения, использующего условную компиляцию. Проблема заключалась в том, что идентификатор не был написан одинаково. Один модуль использовал #if FEATURE_1, в то время как проблемная область использовала #if FEATURE1 (обратите внимание на подчеркивание).

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

2 голосов
/ 05 декабря 2009

разумная цель, но не так велика, как строгое правило

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

Однако, есть много-много-много-много кода, похожего на приведенный ниже пример, и я не думаю, что есть явно лучшая альтернатива. Я думаю, что вы привели разумное руководство, но не великую заповедь «золотой планшет».

#if defined(SOME_IOCTL)
   case SOME_IOCTL:
   ...
#endif
#if defined(SOME_OTHER_IOCTL)
   case SOME_OTHER_IOCTL:
   ...
#endif
#if defined(YET_ANOTHER_IOCTL)
   case YET_ANOTHER_IOCTL:
   ...
#endif
1 голос
/ 08 декабря 2009

Это правда, что #if #endif усложняет чтение кода. Тем не менее, я видел много реального кода, в котором нет проблем с его использованием, и который по-прежнему набирает силу. Так что, возможно, есть лучшие способы избежать использования #if #endif, но их использование не так уж и плохо, если принять надлежащие меры.

1 голос
/ 05 декабря 2009

CPP - это отдельный (не полный по Тьюрингу) язык макросов поверх (обычно) C или C ++. Таким образом, легко запутаться между ним и базовым языком, если вы не будете осторожны. Это обычный аргумент против макросов, а не, например. Шаблоны с ++, в любом случае. Но #ifdef? Просто попробуйте прочитать чужой код, который вы никогда раньше не видели, с кучей ifdef.

например. попробуйте прочитать эти функции Рида-Соломона, помноженные на блок-константу-Галуа: http://parchive.cvs.sourceforge.net/viewvc/parchive/par2-cmdline/reedsolomon.cpp?revision=1.3&view=markup

Если у вас не было следующей подсказки, вам понадобится минута, чтобы выяснить, что происходит: Есть две версии: одна простая и одна с предварительно вычисленной таблицей поиска (LONGMULTIPLY). Тем не менее, получайте удовольствие от отслеживания #if BYTE_ORDER == __LITTLE_ENDIAN. Мне было намного легче читать, когда я переписал этот бит, чтобы использовать функцию le16_to_cpu (чье определение было в предложениях #if), навеянную материалом Linux byteorder.h.

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

0 голосов
/ 02 сентября 2016

Поздний ответ, но я наткнулся на это, ища что-то связанное.

Рассмотрим ситуацию, когда вам необходимо предоставить полностью протестированный код со 100% охватом ветвлений и т. Д. Теперь добавьте условную компиляцию.

Каждый уникальный символ, используемый для управления условной компиляцией, удваивает количество вариантов кода, которые необходимо протестировать. Итак, один символ - у вас есть два варианта. Два символа, теперь у вас есть четыре различных способа компилировать ваш код. И так далее.

И это относится только к логическим тестам, таким как #ifdef. Вы можете легко представить проблему, если тест имеет вид #if VARIABLE == SCALAR_VALUE_FROM_A_RANGE.

0 голосов
/ 05 декабря 2009

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

...