Это плохая практика использовать #ifdef в коде?

Я должен использовать много #ifdef для i386 и x86_64 с для конкретного кода архитектуры и несколько раз #ifdef MAC или # ifdef WIN32... так далее для кода платформы специфического.

мы должны сохранить общую базу кода и портативный.

но мы должны следовать руководству, что использование #ifdef строго нет. Не понимаю почему?

в качестве расширения этого вопроса я также хотел бы понять, когда использовать #ifdef ?

для например, dlopen () не может открыть 32-разрядный двоичный файл во время работы с 64-разрядным процессом и наоборот. Таким образом, его архитектура более специфична. Можем ли мы использовать #ifdef в такой ситуации?

8 ответов


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

вы часто получаете #ifdef используется для целей, отличных от переносимости (определение того, какую "версию" кода производить, например, какой уровень самодиагностики будет включен). К сожалению, они часто взаимодействуют, и переплелись. Например, кто-то, портирующий какой-то код в MacOS, решает, что ему нужна лучшая отчетность об ошибках, которую он добавляет, но делает ее специфичной для MacOS. Позже кто-то еще решает, что лучшая отчетность об ошибках будет ужасно полезна в Windows, поэтому он автоматически включает этот код #defineing MACOS, Если WIN32 определен - но затем добавляет "еще пару"#ifdef WIN32 чтобы исключить какой-то код, что действительно и специфические для macOS, когда Win32 является определенными. Конечно, мы также добавьте тот факт, что MacOS основан на BSD Unix, поэтому, когда macOS определен, он автоматически определяет BSD_44 , но (снова) поворачивается и исключить некоторые "вещи" BSD при компиляции для MacOS.

это быстро вырождается в код, как в следующем примере (взято из #ifdef считается вредным):

#ifdef SYSLOG
#ifdef BSD_42
openlog("nntpxfer", LOG_PID);
#else
openlog("nntpxfer", LOG_PID, SYSLOG);
#endif
#endif
#ifdef DBM
if (dbminit(HISTORY_FILE) < 0)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"couldn’t open history file: %m");
#else
    perror("nntpxfer: couldn’t open history file");
#endif
    exit(1);
}
#endif
#ifdef NDBM
if ((db = dbm_open(HISTORY_FILE, O_RDONLY, 0)) == NULL)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"couldn’t open history file: %m");
#else
    perror("nntpxfer: couldn’t open history file");
#endif
    exit(1);
}
#endif
if ((server = get_tcp_conn(argv[1],"nntp")) < 0)
{
#ifdef SYSLOG
    syslog(LOG_ERR,"could not open socket: %m");
#else
    perror("nntpxfer: could not open socket");
#endif
    exit(1);
}
if ((rd_fp = fdopen(server,"r")) == (FILE *) 0){
#ifdef SYSLOG
    syslog(LOG_ERR,"could not fdopen socket: %m");
#else
    perror("nntpxfer: could not fdopen socket");
#endif
    exit(1);
}
#ifdef SYSLOG
syslog(LOG_DEBUG,"connected to nntp server at %s", argv[1]);
#endif
#ifdef DEBUG
printf("connected to nntp server at %s\n", argv[1]);
#endif
/*
* ok, at this point we’re connected to the nntp daemon
* at the distant host.
*/

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

чтобы дать конкретный пример того, как я бы предпочел, чтобы это было написано, Я бы сделал что-то вроде этого:

if (!open_history(HISTORY_FILE)) {
    logerr(LOG_ERR, "couldn't open history file");
    exit(1);
}

if ((server = get_nntp_connection(server)) == NULL) {
    logerr(LOG_ERR, "couldn't open socket");
    exit(1);
}

logerr(LOG_DEBUG, "connected to server %s", argv[1]);

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

#ifdef SYSLOG
    #define logerr(level, msg, ...) /* ... */
#else
    enum {LOG_DEBUG, LOG_ERR};
    #define logerr(level, msg, ...) /* ... */
#endif

[на данный момент, предполагая препроцессор, который может/будет обрабатывать переменные макросы]

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


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

вы должны избегать #ifdef всякий раз, когда это возможно. IIRC, это был Скотт Майерс, который написал это с #ifdefs вы не получаете независимый от платформы код. Вместо этого вы получаете код, который зависит от нескольких платформ. Также #define и #ifdef не являются частью самого языка. #defines не имеют понятия о области, которая может вызвать всевозможные проблемы. Лучший способ - свести использование препроцессора к минимуму, например включить охрану. В противном случае вы может в конечном итоге с запутанный беспорядок, который очень трудно понять, поддерживать и отлаживать.

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

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

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

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


Я видел 3 широких использования #ifdef:

  • изолировать от конкретной платформы код
  • изолировать специфический код функции (не все версии компиляторов / диалекта языка рождаются равными)
  • изолировать код режима компиляции (NDEBUG кого ?)

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


1. Код платформы

каждая платформа поставляется со своим собственным набором конкретных включает в себя, структуры и функции, чтобы иметь дело с такими вещами, как IO (в основном).

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

в идеале:

project/
  include/namespace/
    generic.h
  src/
    unix/
      generic.cpp
    windows/
      generic.cpp

таким образом, материал платформы все держится вместе в одном файла (в заголовке) так легко найти. The generic.h файл описывает интерфейс,generic.cpp выбирается системой сборки. Нет!--2-->.

если вы хотите встроенные функции (для производительности), то определенный genericImpl.i предоставление встроенных определений и специфики платформы может быть включено в конце generic.h файл с одним #ifdef.


2. Особенность конкретного кода

это становится немного сложнее, но обычно испытано только библиотеками.

например, Boost.MPL намного легче реализовать с компиляторами имея шаблоны с переменным числом аргументов.

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

здесь нет рая. Если окажетесь в такой ситуации... вы в конечном итоге с Boost-как файл (да).


3. Код режима компиляции

вы можете как правило, уйти с парой #ifdef. Традиционный пример:assert:

#ifdef NDEBUG
#  define assert(X) (void)(0)
#else // NDEBUG
#  define assert(X) do { if (!(X)) { assert_impl(__FILE__, __LINE__, #X); } while(0)
#endif // NDEBUG

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

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


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

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


Не уверен, что вы подразумеваете под "#ifdef строго нет", но, возможно, вы имеете в виду политику в проекте, над которым работаете.

вы можете не проверять такие вещи, как Mac или WIN32 или i386. В общем, вам на самом деле все равно, находитесь ли вы на Mac. Вместо этого, есть некоторые функции MacOS, которые вы хотите, и то, что вы заботитесь о наличии (или отсутствии) этой функции. По этой причине в вашей настройке сборки обычно есть сценарий, который проверяет наличие функции и #определяет вещи на основе функций, предоставляемых системой, а не делать предположения о наличии функций на основе платформы. В конце концов, вы можете предположить, что некоторые функции отсутствуют в MacOS, но у кого-то может быть версия MacOS, на которую они перенесли эту функцию. Скрипт, который проверяет наличие таких функций, обычно называется "configure" и часто генерируется autoconf.


лично я предпочитаю хорошо абстрагировать этот шум (когда это необходимо). если это по всему телу интерфейса класса - фу!

Итак, предположим, есть тип, который определяется платформой:

Я буду использовать typedef на высоком уровне для внутренних битов и создавать абстракцию - это часто одна строка на #ifdef/#else/#endif.

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

Итак, просто используйте его, чтобы сосредоточиться на конкретной абстракции платформы вам нужно, и использовать абстракции, чтобы клиентский код был таким же -- так же, как уменьшение объема переменной;)


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


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

Я потерял неделю времени отладки из-за неправильно написанных идентификаторов. Компилятор не выполняет проверку определенных констант в единицах перевода. Например, один блок может использовать "WIN386", а другой - "WIN_386". Макросы платформы-кошмар обслуживания.

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

просто верьте, что они злые и предпочитают не использовать их.