Следует использовать сценарий конфигурации, способный выполнять проверки платформы и генерировать соответствующие флаги компилятора и / или файлы заголовков конфигурации.
Есть несколько инструментов, способных выполнить эту задачу, например autotools , Scons или Cmake .
В вашем случае я бы рекомендовал использовать CMake, так как он прекрасно интегрируется с Windows, имея возможность генерировать файлы проекта Visual Studio, а также make-файлы Mingw.
Основная философия, лежащая в основе этих инструментов, заключается в том, что вы не тестируете заново саму ОС, а проверяете возможности, которые могут присутствовать или отсутствовать, или для которых значения могут варьироваться, что снижает риск того, что ваш код не будет скомпилирован с " платформа не поддерживается »ошибка.
Вот прокомментированный образец CMake (CMakeFiles.txt):
# platform tests
include(CheckFunctionExists)
include(CheckIncludeFile)
include(CheckTypeSize)
include(TestBigEndian)
check_include_file(sys/types.h HAVE_SYS_TYPES_H)
check_include_file(stdint.h HAVE_STDINT_H)
check_include_file(stddef.h HAVE_STDDEF_H)
check_include_file(inttypes.h HAVE_INTTYPES_H)
check_type_size("double" SIZEOF_DOUBLE)
check_type_size("float" SIZEOF_FLOAT)
check_type_size("long double" SIZEOF_LONG_DOUBLE)
test_big_endian(IS_BIGENDIAN)
if(IS_BIGENDIAN)
set(WORDS_BIGENDIAN 1)
endif(IS_BIGENDIAN)
# configuration file
configure_file(config-cmake.h.in ${CMAKE_BINARY_DIR}/config.h)
include_directories(${CMAKE_BINARY_DIR})
add_definitions(-DHAVE_CONFIG_H)
При этом вы должны предоставить шаблон config-cmake.h.in, который будет обработан cmake для создания файла config.h, содержащего определения. вам нужно:
/* Define to 1 if you have the <sys/types.h> header file. */
#cmakedefine HAVE_SYS_TYPES_H
/* Define to 1 if you have the <stdint.h> header file. */
#cmakedefine HAVE_STDINT_H
/* Define to 1 if your processor stores words with the most significant byte
first (like Motorola and SPARC, unlike Intel and VAX). */
#cmakedefine WORDS_BIGENDIAN
/* The size of `double', as computed by sizeof. */
#define SIZEOF_DOUBLE @SIZEOF_DOUBLE@
/* The size of `float', as computed by sizeof. */
#define SIZEOF_FLOAT @SIZEOF_FLOAT@
/* The size of `long double', as computed by sizeof. */
#define SIZEOF_LONG_DOUBLE @SIZEOF_LONG_DOUBLE@
Я приглашаю вас посетить веб-сайт cmake, чтобы узнать больше об этом инструменте.
Я лично фанат cmake, который использую в своих личных проектах.
Один из способов сделать это - поместить заголовки, специфичные для ОС, в разные каталоги и контролировать, какой каталог будет найден, передавая его компилятору с помощью флага -I.
В вашем случае вы бы просто
#include "defs.h"
В использовании препроцессора нет ничего "плохого" по своей сути. Он был создан по какой-то причине, и такая условная компиляция / включение - одна из вещей, которые работают довольно хорошо.
Главное - сохранить читабельность вашего кода. Если в вашем коде много блоков #ifdef WIN32
или вы обнаруживаете, что делаете это в большом количестве файлов, вот несколько советов, как немного очистить ваш код.
1) Переместите свое условное включение в отдельный файл. Ваши исходные файлы будут просто #include
и переместят ваш блок условного включения в defs.h (даже если это все, что есть в этом файле).
2) Пусть ваш make-файл определяет, какой файл включить. Опять же, ваши исходные файлы будут просто #include
. Ваш make-файл определит текущую платформу и при необходимости добавит -iwin32
или -ilinux
в строку параметров компилятора.Я обычно придерживаюсь этого подхода. Вы можете выполнить работу по обнаружению платформы внутри make-файла или на этапе настройки (если вы динамически генерируете свои make-файлы). Я также считаю, что этот вариант является самым простым с точки зрения добавления поддержки новой платформы позднее; кажется, это сводит к минимуму количество необходимых изменений.
3) Если вы строите в системе Linux (подразумевая, что любой код Win32 будет сгенерирован кросс-компилятором), вы можете создать в сценариях сборки символическую ссылку на соответствующий каталог файла заголовка. Ваш код может ссылаться на каталог как #include
и позволить make-файлу или скрипту configure позаботиться о символической ссылке на правильную папку.
Препроцессор сам по себе неплох. Это мощный инструмент, как и многие другие инструменты разработки. Вопрос в том, как вы его используете?
Если вы сделаете предварительную обработку понятной, документированной и логичной, это вообще не проблема. Это становится проблемой, когда вы начинаете выполнять «хитрые» трюки, которые трудно расшифровать.
В идеале вы должны ограничить, как часто вы это делаете. Так что, возможно, у вас есть defs.h
, который вы включаете во все свои файлы, и внутри этого одного файла вы выполняете логику, специфичную для вашей ОС.
Если вы собираетесь использовать препроцессор, то сейчас самое время его использовать. Существуют различные решения, но я считаю, что вы можете решить эту проблему чистым способом.
Обычно я решаю эту проблему так: сначала создаю заголовок, который определяет платформу и компилятор, например:
#define LIB_PLATFORM_WIN32 (1)
#define LIB_PLATFORM_LINUX (2)
#if defined(WIN32)
#define LIB_PLATFORM LIB_PLATFORM_WIN32
#elif defined(LINUX)
#define LIB_PLATFORM LIB_PLATFORM_LINUX
#endif
Теперь у вас есть платформа; затем вы можете просто использовать свои собственные определения во всем коде.
Например, если вам нужно независимое от компилятора 64-битное целое число, вы можете использовать это
#if LIB_COMPILER == LIB_COMPILER_MSVC
#define int64 __int64
#define uint64 unsigned __int64
#else
#define int64 long long
#define uint64 unsigned long long
#endif
Если вы хорошо структурируете его, у вас не должно быть проблем с сохранением чистоты и здравомыслия. :)
Вы также можете использовать typedefs, но это может вызвать другие проблемы, если вы захотите перегрузить тип позже в вашем приложении.
Это действительно зависит от детализации фактических различий между реализациями для конкретных платформ (в вашем коде это соответствует "common stuff below here").
Когда различия значительны, я часто предпочитаю разложить весь платформозависимый код на несколько компонентов (.cpp), имеющих один и тот же платформонезависимый интерфейс (.hpp).
Например, предположим, нам нужно получать изображения с камеры firewire в кроссплатформенной прикладной программе. Способы достижения этой цели на разных платформах безнадежно отличаются. Тогда имеет смысл иметь общий интерфейс, который будет помещен в CameraIeee1394.hpp, а две платформозависимые реализации разместить в CameraIeee1394_unix.cpp и CameraIeee1394_w32.cpp.
Скрипт, подготавливающий makefile для конкретной платформы, может автоматически игнорировать исходные файлы для других платформ, просто проверяя суффикс имени файла.