Подтвердить что ты не робот

Неправильно ли использовать код #ifdef в коде?

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

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

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

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

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

4b9b3361

Ответ 1

Вместо #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

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

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


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

Ответ 2

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

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

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

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

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

Ответ 3

Я видел 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 (aye).


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", тогда вы рискуете изменить поток при некоторых обстоятельствах. Кроме того, макрос, не оценивающий их аргументы, может привести к странному поведению, когда в миксе есть вызовы функций (с побочными эффектами), но в этом случае это желательно, поскольку вовлеченное вычисление может быть дорогостоящим.

Ответ 4

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

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

Ответ 5

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

поэтому, допустим, существует тип, определяемый платформой:

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

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

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

Ответ 6

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

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

Ответ 7

Другие указали предпочтительное решение: поместите зависимый код в отдельный файл, который включен. Это файлы, соответствующие различные реализации могут быть либо в отдельных каталогах (один из который задается с помощью директивы -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_ префиксом, который вы обычно используете для макросов.

Ответ 8

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

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

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

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