Кросс-платформенный код С++ и один заголовок - несколько реализаций

Я слышал, что для написания кода Cross Platform С++ необходимо определить классы следующим образом (например, класс Window):

window.h
window_win32.cpp
window_linux.cpp
window_osx.cpp

а затем соответствующим образом выберите файл реализации. Но что, если у меня есть члены этого класса, относящиеся к os? Как член HWND для реализации Win32. Я не могу поместить его в window.h или когда я попытаюсь скомпилировать его, скажем, в Linux, он создаст ошибку компилятора.

Мне нужно #ifdef его? Я уже задал аналогичный вопрос, но этот вопрос больше сосредоточен на этой конкретной проблеме.

Ответ 1

Существует больше способов решить эту проблему - у каждого есть свои плюсы и минусы.

1.) Используйте макросы #ifdef, #endif

// Note: not sure if "WINDOWS" or "WIN32" or something else is defined on Windows
#ifdef WINDOWS
    #include <window.h>
#else
    // etc.
#endif

class MyClass
{
public:

    // Public interface...

private:

#ifdef WINDOWS
    HWND m_myHandle;
#else
    // etc.
#endif

};

Плюсы:

  • Максимальная скорость программы.

Минусы:

  • Худшая читаемость. На многих платформах это может стать очень грязным.
  • Включая специфику платформы, может сломать что-то. windows.h определяет много макросов с нормальными именами.

2.) Как уже было написано, вы можете использовать полиморфизм:

// IMyClass.h for user of your class:

class IMyClass
{
public:

    virtual ~IMyClass() {}
    virtual void doSomething() = 0;

};


// MyClassWindows.h is implementation for one platform

#include <windows.h>
#include "IMyClass.h"

class MyClassWindows : public IMyClass
{
public:

    MyClassWindows();
    virtual void doSomething();

private:

    HWND m_myHandle;

};

// MyClassWindows.cpp implements methods for MyClassWindows

Плюсы:

  • Многое, гораздо более чистый код.

Минусы:

  • Пользователь не может создавать экземпляры вашего класса напрямую (особенно не в стеке).
  • Вы должны предоставить специальную функцию для создания: например, объявить IMyClass * createMyClass(); и определить его в MyClassWindows.cpp и других определенных для платформы файлах. В этом случае (ну, на самом деле, во всем этом случае полиморфизма) вы также должны определить функцию, которая уничтожает экземпляры - чтобы сохранить идиому "кто бы ее не создал, должен также уничтожить".
  • Небольшое замедление из-за виртуальных методов (в наши дни практически совершенно незначительные, кроме очень, очень особых случаев).
  • Примечание. Распределение может быть проблемой на платформах с ограниченной памятью из-за проблем с фрагментацией ОЗУ. В этом случае его можно решить, используя какой-то пул памяти для ваших объектов.

3). Идиома PIMPL.

// MyClass.h

class MyClass
{
public:

    MyClass();
    void doSomething();

private:

    struct MyClassImplementation;

    MyClassImplementation *m_impl;

}


// MyClassWindows.h

#include <windows.h>
#include "MyClass.h"

struct MyClassImplementation
{
    HWND m_myHandle;

    void doSomething();

}

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

Плюсы:

  • Пользователь может создавать экземпляры вашего класса (в том числе в стеке) (не беспокоясь о un/deleted poiners).
  • В MyClass.h не обязательно включать заголовки конкретной платформы.
  • Вы можете просто добавить счетчик ссылок и реализовать совместное использование данных и/или копирование на запись, которые могут легко позволить использовать ваш класс в качестве возвращаемого значения, даже если он хранит большой объем данных.

Минусы:

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

4.) Определите нейтральный тип, который достаточно велик, чтобы хранить ваши данные. Например, long long int.

// MyClass.h

class MyClass
{
public:

    MyClass();
    void doSomething();

private:

    typedef unsigned long long int MyData;

    MyData m_data;

};

В реализации (например, MyClassWindows.cpp) вам всегда нужно использовать (переинтерпретировать кастинг) между MyClass:: MyData и фактическими сохраненными данными.

Плюсы:

  • Как быстро, как первый, но вы избегаете макросов.
  • Избегайте выделения, если не нужно.
  • Предотвращение вызовов нескольких методов.
  • Предотвращение включения заголовков платформы в MyClass.h.

Минусы:

  • Вы должны быть на 110% уверены, что размер MyClass:: MyData всегда по крайней мере такой же, как и данные.
  • Если в другой платформе хранятся данные разного размера, вы откладываете пространство (если вы используете несколько элементов, это нормально, но с миллионами их...), если вы не используете макросы. В этом случае это будет не так грязно.
  • Это низкоуровневая работа с данными - небезопасная, а не ООП. Но быстро.

Итак, используйте тот, который лучше всего подходит для вашей проблемы... и ваша личность: 3, потому что сегодня сила - все четыре более или менее относительно равные по скорости и пространству.

Ответ 2

Точка такого подхода заключается в том, что вы инкапсулируете данные конкретной ОС в специальный файл os. Если вам нужно пройти HWND, вы можете пересмотреть свой дизайн объекта. Wether такой strcuture имеет смысл, зависит от того, насколько большой ваш специальный код. Вы действительно не хотите сжимать все возможные классы в один файл.

С другой стороны, существуют библиотеки для графических интерфейсов, которые делают именно это: инкапсулирование отдельных частей ОС в библиотеку, например QT или wxWidgets или другие. Если вы правильно отделите графический интерфейс от основного кода, вам может и не понадобиться этот подход.

Я использую такую ​​структуру в своем проекте для поддержки разных версий xerces, не имея помех maincode с #ifdefs. Однако в моем случае затронутый код довольно мал. Я могу себе представить, что инфраструктура GUI занимает гораздо больше места.

Ответ 3

Я бы добавил только 5) пулю к ответу @Laethnes

5) Напишите пустой заголовок, который включает во время компиляции заголовок платформы и скрыть класс, специфичный для платформы, под типом typedef

// MyClass.hpp

#if defined(WINDOWS)
#    include "WINMyClass.hpp"
     typedef WINMyClass MyClass
#elif defined(OSX)
#    include "OSXMyClass.hpp"
     typedef OSXMyClass MyClass

   ...  // keep going

#endif

Плюсы:

  • это только typedef, очень просто
  • хорошая читаемость
  • скорость

Минусы:

  • это только typedef

Ответ 4

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

class thread
{
    virtual ~thread() {}
    virtual void run() = 0;
    /* whatever */
};

а затем вы можете наследовать этот класс, используя специфичные для платформы функции:

class posix_thread : thread;

во время компиляции вы выбираете #ifdef, какой класс вы включаете и создаете экземпляр.

Ответ 5

В большинстве случаев вы избегаете таких вещей в заголовке. Там однако, когда вы хотите их разоблачить. В таком случаев, моим решением всегда было использовать что-то вроде:

#include dependentInclude(syst,MyInclude.lhh)

dependInclude - макрос, который расширяется до пути файл, который мне нужен, в зависимости от значения syst (который был установлен в командной строке).