#c #macos #coding-style
#c #macos #стиль кодирования
Вопрос:
Мне приходится использовать много #ifdef i386 и x86_64 для кода, специфичного для архитектуры, и иногда #ifdef MAC или #ifdef WIN32… и так далее для кода, зависящего от платформы.
Мы должны сохранить общую базу кода и переносимость.
Но мы должны следовать рекомендациям, согласно которым использование #ifdef строго запрещено. Я не понимаю, почему?
В качестве дополнения к этому вопросу я также хотел бы понять, когда использовать #ifdef?
Например, dlopen() не может открыть 32-разрядный двоичный файл при запуске из 64-разрядного процесса и наоборот. Таким образом, он более специфичен для архитектуры. Можем ли мы использовать #ifdef в такой ситуации?
Комментарии:
1. что вы подразумеваете под строгим нет?
2. Все обобщения неверны, даже это. Требуется больше контекста: используете ли вы #ifdef для изоляции кода, зависящего от платформы или архитектуры ( повышающего переносимость), или по какой-либо другой причине?
3. Нет от нашего ведущего. Он не хочет, чтобы в коде был #ifdef.
4. Возможно, вы захотите прочитать, Ifdef считается вредным .
5. Конечно, это плохая практика. Гораздо лучше, если он используется в коде, который вам не нужно поддерживать. Как и boost, версия v1.44 содержит 831 файл, содержащий #ifdef.
Ответ №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 %sn", 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 верхнего уровня, который просто выбирает, какие другие make-файлы вызывать, на основе указанного целевого объекта.
- Некоторые люди предпочитают этого не делать. На самом деле я не спорю так или иначе о том, как структурировать make-файлы, просто отмечая, что это возможность, которую некоторые люди находят / считают полезной.
Ответ №2:
Вы должны избегать #ifdef
, когда это возможно. IIRC, это был Скотт Мейерс, который написал, что с #ifdef
s вы не получаете независимый от платформы код. Вместо этого вы получаете код, который зависит от нескольких платформ. Также #define
и #ifdef
не являются частью самого языка. #define
у s нет понятия области видимости, что может вызвать всевозможные проблемы. Лучший способ — свести использование препроцессора к минимуму, например, включить защиту. В противном случае вы, скорее всего, получите запутанный беспорядок, который очень сложно понять, поддерживать и отлаживать.
В идеале, если вам нужно иметь объявления для конкретной платформы, у вас должны быть отдельные каталоги include для конкретной платформы и обрабатывать их соответствующим образом в вашей среде сборки.
Если у вас есть реализация определенных функций для конкретной платформы, вам также следует поместить их в отдельные файлы .cpp и снова хэшировать их в конфигурации сборки.
Другой возможностью является использование шаблонов. Вы можете представлять свои платформы пустыми фиктивными структурами и использовать их в качестве параметров шаблона. Затем вы можете использовать специализацию шаблона для кода, зависящего от платформы. Таким образом, вы будете полагаться на компилятор для генерации кода для конкретной платформы из шаблонов.
Конечно, единственный способ, чтобы все это работало, — это очень четко разделить код, зависящий от платформы, на отдельные функции или классы.
Комментарии:
1. Как насчет кода из другой архитектуры? Допустим, загрузка двоичного файла dylib? Должно ли это быть отдельным для архитектуры?
2. @MacGeek: Что такое двоичный файл dylib? DLL или .so? Вы ссылаетесь на него?
3. И как бы вы предоставили правильную фиктивную структуру платформы без
#ifdef
s?4. @Xeo у вас был бы заголовочный файл для конкретной платформы с
typedef
.5. @MacGeek что ж, тогда, если вы ссылаетесь на dll, это зависит от среды сборки. Если вы загружаете его динамически в свой код, то это должно выполняться в функции или классе, зависящих от платформы.
Ответ №3:
Я видел 3 широких использования #ifdef
:
- изолировать код, зависящий от платформы
- изолировать код, специфичный для конкретной функции (не все версии компиляторов / диалектов языка рождаются одинаковыми)
- изолировать код режима компиляции (
NDEBUG
кто-нибудь?)
Каждый из них потенциально может создать огромный беспорядок в неуправляемом коде, и с ним следует обращаться соответствующим образом, но не со всеми из них можно обращаться одинаково.
1. Код, зависящий от платформы
Каждая платформа поставляется со своим собственным набором конкретных включений, структур и функций для работы с такими вещами, как ввод-вывод (в основном).
В этой ситуации самый простой способ справиться с этим беспорядком — представить единый интерфейс и использовать реализации, зависящие от платформы.
В идеале:
project/
include/namespace/
generic.h
src/
unix/
generic.cpp
windows/
generic.cpp
Таким образом, все содержимое платформы хранится вместе в одном файле (для каждого заголовка), который так легко найти. generic.h
Файл описывает интерфейс, generic.cpp
выбирается системой сборки. Нет #ifdef
.
Если вам нужны встроенные функции (для повышения производительности), то конкретный, 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», тогда вы рискуете изменить поток при некоторых обстоятельствах. Кроме того, макрос, не оценивающий свои аргументы, может привести к странному поведению при наличии вызовов функций (с побочными эффектами) в сочетании, но в данном случае это желательно, поскольку связанные с этим вычисления могут быть дорогостоящими.
Ответ №4:
Многие программы используют такую схему для создания кода, специфичного для платформы. Лучший способ, а также способ очистки кода, заключается в том, чтобы поместить весь код, специфичный для одной платформы, в один файл, назвав функции одинаковыми и имея одинаковые аргументы. Затем вы просто выбираете, какой файл создавать, в зависимости от платформы.
Возможно, все еще остались некоторые места, где вы не можете извлечь код, зависящий от платформы, в отдельные функции или файлы, и вам все еще могут понадобиться #ifdef
части, но, надеюсь, это должно быть сведено к минимуму.
Комментарии:
1. Самое замечательное в этом то, что какой бы ведущий ни сказал «#idfef — это строгий запрет», проблема возвращается к ним, потому что им нужно будет решить, как проект будет структурировать свой код для конкретной платформы и как настроить сборку для компиляции и связывания правильных файлов для конкретной платформы с правильным путем включения для конкретной платформы. Это дает им возможность объяснить свои принципы, вместо того, чтобы устанавливать запрет, а затем уходить вдаль 😉
Ответ №5:
Я предпочитаю разбивать зависящий от платформы код и функции на отдельные единицы перевода и позволять процессу сборки решать, какие единицы использовать.
Я потерял неделю времени отладки из-за неправильно написанных идентификаторов. Компилятор не выполняет проверку определенных констант в единицах перевода. Например, один модуль может использовать «WIN386», а другой «WIN_386». Макросы платформы — это кошмар для обслуживания.
Кроме того, при чтении кода вы должны проверить инструкции по сборке и заголовочные файлы, чтобы увидеть, какие идентификаторы определены. Также существует разница между существующим идентификатором и значением. Какой-то код может проверять наличие идентификатора, в то время как другой проверяет значение того же идентификатора. Последний тест не определен, когда идентификатор не указан.
Просто верьте, что они злые, и предпочитаете их не использовать.
Ответ №6:
Не уверен, что вы подразумеваете под «#ifdef — это строгое нет», но, возможно, вы имеете в виду политику в проекте, над которым вы работаете.
Вы могли бы подумать о том, чтобы не проверять такие вещи, как Mac, WIN32 или i386. В общем, вам на самом деле все равно, используете ли вы Mac. Вместо этого в macOS есть какая-то функция, которая вам нужна, и вас волнует наличие (или отсутствие) этой функции. По этой причине обычно в вашей настройке сборки есть скрипт, который проверяет наличие функций и #определяет вещи на основе функций, предоставляемых системой, вместо того, чтобы делать предположения о наличии функций на основе платформы. В конце концов, вы можете предположить, что определенные функции отсутствуют в macOS, но у кого-то может быть версия macOS, на которую они перенесли эту функцию. Скрипт, который проверяет наличие таких функций, обычно называется «configure», и он часто генерируется autoconf.
Ответ №7:
лично я предпочитаю хорошо абстрагироваться от этого шума (там, где это необходимо). если это по всему телу интерфейса класса — фу!
итак, допустим, есть тип, который определяется платформой:
Я буду использовать typedef на высоком уровне для внутренних битов и создам абстракцию — это часто одна строка на #ifdef
/ #else
/ #endif
.
тогда для реализации я также буду использовать один #ifdef
для этой абстракции в большинстве случаев (но это означает, что определения, относящиеся к конкретной платформе, появляются один раз для каждой платформы). Я также разделяю их на отдельные файлы, зависящие от платформы, чтобы я мог перестроить проект, добавив все исходные тексты в проект и собрав без сбоев. В этом случае #ifdef
также удобнее, чем пытаться определить все зависимости для каждого проекта, для каждой платформы, для каждого типа сборки.
Итак, просто используйте его, чтобы сосредоточиться на абстракции конкретной платформы, которая вам нужна, и используйте абстракции, чтобы клиентский код был таким же — точно так же, как уменьшение области видимости переменной 😉
Ответ №8:
Другие указали предпочтительное решение: поместить зависимый код в отдельный файл, который включен. При этом файлы, соответствующие различным реализациям, могут либо находиться в отдельных каталогах (один из которых указывается с помощью -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_
префиксом, который вы обычно используете для макросов.