Что является общей идиомой для абстрактных кросс-платформенных реализаций?

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

class Operation {
    DoOperation() = 0;
}

class OperationPlatform1 : public Operation {
    DoOperation();
}

class OperationPlatform2 : public Operation {
    DoOperation();
}

#ifdef USING_PLATFORM1
    Operation *op = new OperationPlatform1;
#endif

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

class OperationPlatform1 {
    DoOperation();
}

class OperationPlatform2 {
    DoOperation();
}

#ifdef USING_PLATFORM1
typedef OperationPlatform1 Operation;
#endif

Operation op;

Каков хороший способ абстрагировать несколько реализаций, из которых только одна будет выбрана во время компиляции? Меня также интересует производительность, поэтому я бы не хотел использовать виртуальные методы, если в этом нет необходимости.


person Dan Nestor    schedule 13.01.2014    source источник
comment
Использовать исключительно стандартный С++.   -  person PlasmaHH    schedule 13.01.2014


Ответы (5)


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

Чрезмерное использование #ifdef обычно является признаком плохого дизайна. См. https://www.usenix.org/legacy/publications/library/proceedings/sa92/spencer.pdf.

person James Kanze    schedule 13.01.2014

У вас есть несколько вариантов:

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

  2. Просто создайте один класс и используйте #ifdef, чтобы вставить правильный код для соответствующей платформы.

  3. Используйте уже существующую библиотеку-оболочку (например, Boost), которая может уже обертывать биты, специфичные для платформы, и код для обертки.

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

person Sean    schedule 13.01.2014

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

Вы могли бы иметь в своем заголовке:

class Operation {
    DoOperation();
}

И множественный cpp вы можете исключить файлы из вашей сборки:

Платформа1.cpp

Operation::DoOperation() { // Platform 1 implementation }

и Platform2.cpp

Operation::DoOperation() { // Platform 2 implementation }



Или отдельный файл реализации:

#ifdef PLATFORM1
    Operation::DoOperation() { // Platform 1 implementation }
elseif defined(PLATFORM2)
    Operation::DoOperation() { // Platform 2 implementation }
#endif



Или смесь 2, если вы не хотите возиться с исключением файлов из сборки:

Платформа1.cpp:

#ifdef PLATFORM1
    Operation::DoOperation() { // Platform 1 implementation }
#endif

и Platform2.cpp:

#ifdef PLATFORM2
    Operation::DoOperation() { // Platform 2 implementation }
#endif
person Eric Fortin    schedule 13.01.2014

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

В то же время вы можете использовать #ifdef для реализации. Например, функции файловой системы очень сильно зависят от платформы, поэтому вы должны определить такой интерфейс файловой системы.

class IFileSystem {
public:
    void CopyFile(const string& destName, const string& sourceName);
    void DeleteFile(const string& name);
    // etc.
};

Тогда у вас могут быть реализации для WindowsFileSystem, LinuxFileSystem, SolarisFileSystem и т. д.

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

IFileSystem& GetFileSystem() {
   #ifdef WINDOWS
       return WindowsFileSystemInstance;
   #endif
   #ifdef LINUX
       return LinuxFileSystemInstance;
   #endif
   // etc.
}

GetFileSystem().CopyFile("dest", "source");

Этот подход помогает вам разделить проблемы, связанные с логикой приложения и зависимостями от платформы.

Дополнительным преимуществом функции getter является то, что у вас все еще есть возможность делать выбор во время выполнения для реализации файловой системы, например. Fat32FileSystem, NtfsFileSystem, Ext3FileSystem и т. д.

person Kit Fisto    schedule 13.01.2014

Взгляните на существующие мультиплатформенные фреймворки, например, на Qt. Используйте идиому PIMPL.

person Nikita    schedule 13.01.2014
comment
Идиома брандмауэра компиляции с отдельным исходным файлом для каждой реализации — очень хорошая идея. - person James Kanze; 13.01.2014