Кросс-платформенная архитектура кода на С++

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

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

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

EDIT: в ответ тем, кто предлагает Qt и тому подобное, да, я намеренно искал "изобретать колесо", поскольку я не просто занимаюсь разработкой приложения, но также заинтересован в интеллектуальном вызове собственную библиотеку абстракции платформы. Спасибо за предложение, хотя!

Ответы

Ответ 1

Я использую нейтральные файлы заголовков платформы, сохраняя в исходных файлах определенный код платформы (с использованием

Ответ 2

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

То есть, если вы представляете компонент, Foo, он должен быть специфичным для платформы (например, сокеты или элементы GUI), но имеет следующие публичные элементы:

class Foo {
public:
  void write(const char* str);
  void close();
};

Каждый модуль, который должен использовать Foo, очевидно, имеет #include "Foo.h", но в определенном для платформы файле make вы можете иметь -IWin32, что означает, что компилятор выглядит в .\Win32 и находит конкретный Windows Foo.h, который содержит класс, с тем же интерфейсом, но, возможно, с отдельными частными членами Windows и т.д.

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

Ответ 3

Посмотрите ACE. Он имеет довольно хорошую абстракцию с использованием шаблонов и наследования.

Ответ 4

Я мог бы перейти к типу политики:

template<typename Platform>
struct PlatDetails : private Platform {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + getName();
    }
};

// For any serious compatibility functions, these would
// of course have to be in different headers, and the implementations
// would call some platform-specific functions to get precise
// version numbers. Using PImpl would be a smart idea for these 
// classes if they need any platform-specific members, since as 
// Joe Gauterin says, you want to avoid your application code indirectly
// including POSIX or Windows system headers, containing useless definitions.
struct Windows {
    std::string getName() const { return "Windows"; }
};

struct Linux {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef PlatDetails<Windows> PlatformDetails;
#else
    typedef PlatDetails<Linux> PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

Там не так много, чтобы выбирать между тем, чтобы сделать это, и делать регулярное симулированное динамическое связывание с CRTP, так что общая вещь является базой и конкретной вещью производного класса:

template<typename Platform>
struct PlatDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            static_cast<Platform*>(this)->getName();
    }
};

struct Windows : PlatDetails<Windows> {
    std::string getName() const { return "Windows"; }
};

struct Linux : PlatDetails<Linux> {
    std::string getName() const { return "Linux"; }
};

#ifdef WIN32
    typedef Windows PlatformDetails;
#else
    typedef Linux PlatformDetails;
#endif

int main() {
    std::cout << PlatformDetails().getName() << "\n";
}

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

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

На практике, однако, я согласен с quamrana, что обычно вы можете просто иметь разные реализации одной и той же вещи на разных платформах:

// Or just set the include path with -I or whatever
#ifdef WIN32
    #include "windows/platform.h"
#else
    #include "linux/platform.h"
#endif

struct PlatformDetails {
    std::string getDetails() const {
        return std::string("MyAbstraction v1.0; ") + 
            porting::getName();
    }
};

// windows/platform.h
namespace porting {
    std::string getName() { return "Windows"; }
}

// linux/platform.h
namespace porting {
    std::string getName() { return "Linux"; }
}

Ответ 5

Вы также можете взглянуть на Poco:

Библиотеки POCO C++ (POCO означает POrtable COmponents) - это библиотеки классов с открытым исходным кодом C++, которые упрощают и ускоряют разработку ориентированных на сеть портативных приложений в C++. Библиотеки прекрасно интегрируются со стандартной библиотекой C++ и заполняют многие из оставленных ею функциональных пробелов. Их модульная и эффективная конструкция и реализация делают библиотеки POCO C++ чрезвычайно подходящими для встраиваемых разработок, область, где язык программирования C++ становится все более популярным, благодаря его пригодности для обоих низкоуровневых (устройства ввода-вывода, обработчики прерываний и т.д.) и высокоуровневая объектно-ориентированная разработка. Конечно, библиотеки POCO C++ также готовы к решению задач на уровне предприятия.

poco architecture
(источник: pocoproject.org)

Ответ 6

Если вам нравится использовать полномасштабную фреймворк С++, доступный для многих платформ и разрешающий copylefted, используйте Qt.

Ответ 7

Итак... вы не хотите просто использовать Qt? Для реальной работы с использованием С++ я очень рекомендую ее. Это абсолютно отличный кросс-платформенный инструментарий. Я только что написал несколько плагинов, чтобы заставить его работать над Kindle, а теперь и Palm Pre. Qt делает все легко и весело. Вполне омолаживающий, даже. Ну, пока вы не встретитесь с QModelIndex, но они предположительно поняли, что они переработали его, и они заменяют его;)

Как академическое упражнение, это интересная проблема. Как колесо повторно изобретатель себя, я даже сделал это несколько раз сейчас.:)

Короткий ответ: я бы пошел с PIMPL. (Источники Qt имеют примеры a-много)

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

В обоих случаях у меня было очень сильное чувство, что я перехитрил и потерял свой путь.

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

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

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

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

Однако в качестве общего руководства... начните с PIMPL.

Ответ 8

Есть также большие мальчики, такие как Qt4 (полная структура + GUI), GTK + (gui-only afaik) и Boost (только для рамки, без GUI), все 3 поддерживают большинство платформ, GTK + - это C, Qt4/Boost - это С++ и по большей части основаны на шаблонах.