Я пишу программу, которая стремится быть кроссплатформенной; как таковой, он будет поддерживать несколько реализаций определенных операций. Моей первой идеей было написать иерархию классов с общим интерфейсом, возможно, абстрактной фабрикой для каждой платформы.
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;
Каков хороший способ абстрагирования нескольких реализаций, из которых во время компиляции будет выбрана только одна? Я также заинтересован в производительности, поэтому я не хотел бы использовать виртуальные методы без необходимости.
Обычное решение состоит в том, чтобы определить только один класс в
заголовок, и предоставить различные исходные файлы для него, компилируя
и связывание того, что необходимо. Если вам нужно немного
зависимости (например, типы данных) в определении класса, вы также можете
добиться этого, создав отдельный заголовочный файл для каждого
платформа, и в том числе. Самое простое решение здесь
вероятно, создать каталог для целевой платформы и поставить все
зависимых от платформы файлов, используя -I
//I
подобрать
до правильного каталога (и, следовательно, правильной платформы
зависимые файлы) во время компиляции.
Чрезмерное использование #ifdef
обычно это признак плохого дизайна. Увидеть
https://www.usenix.org/legacy/publications/library/proceedings/sa92/spencer.pdf.
У вас есть несколько вариантов:
Используйте подход, изложенный в вашем вопросе. Тем не менее, скорее всего, вам нужно будет разбить биты, специфичные для платформы, на отдельные файлы и использовать правило сборки, чтобы ввести правильный файл для платформы.
Просто есть один класс и использовать #ifdef
вставить правильный код для соответствующей платформы.
Используйте уже существующую библиотеку оболочки (например, Boost), которая может уже обернуть специфичные для платформы биты, и код против оболочки.
Если вы собираетесь инкапсулировать различные платформы, вам нужно убедиться, что абстракции не проникают в вашу реализацию. Часто в конечном итоге вы используете комбинацию из трех перечисленных предметов.
То, что у вас здесь, обычно используется. Если вы абсолютно уверены, что у вас никогда не будет нескольких реализаций одновременно, вы можете использовать один заголовочный файл и либо иметь условно скомпилированный файл реализации (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
То, что вы хотите, это обеспечить поддержку различных платформ, сохраняя при этом исходный код в чистоте. Поэтому вам следует абстрагировать зависимости 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 и т. Д.
Взгляните на существующие мультиплатформенные фреймворки, например, Qt. использование PIMPL идиома