Это плохая практика использовать #ifdef в коде? - PullRequest
16 голосов
/ 04 ноября 2011

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

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

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

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

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

Ответы [ 8 ]

13 голосов
/ 04 ноября 2011

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

Вы также часто получаете #ifdef, который используется для целей, отличных от переносимости (определяя, что«версия» создаваемого кода, например, какой уровень самодиагностики будет включен).К сожалению, оба часто взаимодействуют и переплетаются.Например, кто-то портирует некоторый код на MacOS и решает, что ему нужно лучше сообщать об ошибках, что он добавляет, но делает это специфичным для MacOS.Позже, кто-то другой решает, что лучшее сообщение об ошибках было бы очень полезно в Windows, поэтому он включает этот код, автоматически #define используя 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

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

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


  1. Некоторые люди предпочитают этого не делать.Я не спорю так или иначе о том, как структурировать make-файлы, просто отмечаю, что некоторые люди считают / считают это полезным.
7 голосов
/ 04 ноября 2011

Я видел 3 широких употребления #ifdef:

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

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


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

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

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

В идеале:

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

Таким образом, все содержимое платформы хранится в одном файле (на заголовок), поэтому его легко найти. Файл generic.h описывает интерфейс, generic.cpp выбирается системой сборки. № #ifdef.

Если вам нужны встроенные функции (для повышения производительности), то в конец файла generic.h может быть добавлен отдельный genericImpl.i, содержащий встроенные определения и специфику платформы, с одним #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

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

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

7 голосов
/ 04 ноября 2011

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

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

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

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

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

2 голосов
/ 04 ноября 2011

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

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

1 голос
/ 04 ноября 2011

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

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

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

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

1 голос
/ 04 ноября 2011

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

#include XX_dependentInclude(config.hh)

(В этом случае XX_dependentInclude может быть определено как что-то вроде:

#define XX_string2( s ) # s
#define XX_stringize( s ) XX_string2(s)
#define XX_paste2( a, b ) a ## b
#define XX_paste( a, b ) XX_paste2( a, b )
#define XX_dependentInclude(name) XX_stringize(XX_paste(XX_SYST_ID,name))

и SYST_ID инициализируются с использованием -D или /D в компиляторе призывание.)

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

1 голос
/ 04 ноября 2011

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

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

1 голос
/ 04 ноября 2011

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...