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

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

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;

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

3

Решение

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

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

5

Другие решения

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

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

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

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

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

2

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

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

class Operation {
DoOperation();
}

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

Platform1.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 если вы не хотите возиться с исключением файлов из сборки:

Platform1.cpp:

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

и Platform2.cpp:

#ifdef PLATFORM2
Operation::DoOperation() { // Platform 2 implementation }
#endif
2

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

В то же время вы можете использовать #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 и т. Д.

1

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

0
По вопросам рекламы [email protected]