С++: зависимые от платформы типы - лучший шаблон
Я ищу шаблон для организации заголовочных файлов для нескольких платформ на С++.
У меня есть файл-оболочка .h, который должен компилироваться под Linux и Win32.
Это лучшее, что я могу сделать?
// defs.h
#if defined(WIN32)
#include <win32/defs.h>
#elif defined(LINUX)
#include <linux/defs.h>
#else
#error "Unable to determine OS or current OS is not supported!"
#endif
// Common stuff below here...
Мне действительно не нравится материал препроцессора на С++. Есть ли чистый (и нормальный) способ сделать это лучше?
Ответы
Ответ 1
Вы должны использовать конфигурацию script, способную выполнять проверки платформы и генерировать соответствующие флаги компилятора и/или файлы заголовков конфигурации.
Есть несколько инструментов, которые могут выполнять эту задачу, например 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 @[email protected]
/* The size of `float', as computed by sizeof. */
#define SIZEOF_FLOAT @[email protected]
/* The size of `long double', as computed by sizeof. */
#define SIZEOF_LONG_DOUBLE @[email protected]
Я приглашаю вас посетить сайт cmake, чтобы узнать больше об этом инструменте.
Я лично поклонник cmake, который я использую для своих личных проектов.
Ответ 2
Один из способов сделать это - установить заголовки, специфичные для ОС, в разные каталоги и определить, какой каталог найден, передав этот каталог компилятору через флаг -I.
В вашем случае вы просто имели бы
#include "defs.h"
Ответ 3
Нет ничего по своей сути "плохо" в использовании препроцессора. Он был создан по какой-то причине, и условная компиляция/включение, подобная этому, является одной из вещей, которые делают довольно хорошо.
Ключ должен сохранять читаемый код. Если в вашем коде есть много блоков #ifdef WIN32
, или если вы обнаружите, что делаете это в большом количестве файлов, вот несколько советов по очистке вашего кода.
1) Переместите условное включение в отдельный файл. Ваши исходные файлы будут просто #include <defs.h>
и переместите ваш условный блок включения в defs.h(даже если все это в этом файле).
2) Пусть ваш makefile определит, какой файл включить. Опять же, ваши исходные файлы будут просто #include <defs.h>
. Ваш make файл обнаружит текущую платформу и добавит -iwin32
или -ilinux
в строку параметров компилятора, если это необходимо. Это подход, который я обычно беру. Вы можете выполнять работу по обнаружению платформы внутри make файла или на этапе конфигурации (если вы динамически генерируете свои файлы make файлов). Я также считаю этот вариант самым легким с точки зрения добавления поддержки новой платформы на более поздний срок; это минимизирует количество изменений, которые необходимо внести.
3) Если вы строите на системе Linux (подразумевая, что любой код Win32 будет сгенерирован кросс-компилятором), вы можете создать скрипты сборки для символической ссылки на соответствующий каталог файлов заголовков. Ваш код может ссылаться на каталог как #include <platform\defs.h>
и позволить makefile или configure script беспокоиться о символической привязке к правильной папке.
Ответ 4
Препроцессор - это не плохая вещь сама по себе. Это мощный инструмент, как и многие другие инструменты для разработки. Вопрос в том, как вы его используете?
Если вы делаете свою предварительную обработку прозрачной, документированной и логичной, это не проблема вообще. Это когда вы начинаете делать "умные" трюки, которые трудно понять, что это становится проблемой.
В идеале вы бы ограничили, как часто вы это делаете. Таким образом, возможно, у вас есть defs.h
, который вы включите во все ваши файлы, и внутри этого одного файла вы будете использовать свою конкретную логику ОС.
Ответ 5
Это действительно зависит от детализации фактических различий между реализациями для определенных платформ (в вашем коде это соответствует "общим словам ниже" ).
Когда различия значительны, я часто предпочитаю учитывать все зависящие от платформы коды к нескольким компонентам (.cpp), использующим один и тот же платформенный независимый интерфейс (.hpp).
Например, предположим, что нам нужно получить изображения с камеры FireWire в кросс-платформенной прикладной программе. Пути достижения этого на разных платформах безнадежно отличаются. Тогда имеет смысл иметь общий интерфейс, который будет помещен в CameraIeee1394.hpp, и поместить две зависимые от платформы реализации в CameraIeee1394_unix.cpp и CameraIeee1394_w32.cpp.
script, который подготавливает make файл для конкретной платформы, может автоматически игнорировать исходные файлы для иностранных платформ, просто проверяя суффикс имени файла.
Ответ 6
Если вы собираетесь использовать препроцессор, настало время его использовать. Существуют разные решения, но я считаю, что вы можете решить это чистым способом.
Как я обычно решаю это, я сначала создаю заголовок, который обнаруживает платформу и компилятор, например:
#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, но это может привести к другим проблемам, если вы хотите перегрузить тип позже в своем приложении.