Я пытаюсь создать общий интерфейс для двух сторонних библиотек, которые имеют схожую функциональность, чтобы я мог кодировать против абстрактного интерфейса и выбирать во время компиляции, какую реализацию использовать.
Мне нужен этот абстрактный интерфейс, чтобы не добавлять никаких накладных расходов, то есть о полиморфизме не может быть и речи. В любом случае это не нужно, так как используется только одна фактическая реализация. Итак, моя первоначальная попытка выглядела так:
AbstractInterface.h:
// Forward declarations of abstract types.
class TypeA;
class TypeB;
class TypeC;
TypeA *foo(TypeA *a, TypeB *b);
TypeB *bar(std::vector<TypeC*> &c);
TypeC *baz(TypeC *c, TypeA *c);
ImplementationOne.cpp:
class ActualTypeA {...};
using TypeA = ActualTypeA; // Error!
...
К сожалению, это приводит к ошибке компиляции, говоря о том, что TypeA переопределяется с использованием различных типов, хотя предварительное объявление ничего не сообщало ему, кроме того, что это класс. Итак, следующее, что я попробовал, было так:
class TypeA : public ActualTypeA {}; // No more error
...
TypeA *foo(TypeA *a, TypeB *b)
{
return actualFoo(a, b); // Error
}
Здесь actualFoo () возвращает ActualTypeA *, который не может быть автоматически преобразован в TypeA *. Поэтому я должен переписать его в нечто вроде:
inline TypeA *A(ActualTypeA *a)
{
return reinterpret_cast<TypeA*>(a);
}
TypeA *foo(TypeA *a, TypeB *b)
{
return A(actualFoo(a, b));
}
Причина, по которой я использую вспомогательную функцию A (), заключается в том, что я случайно не приводил что-то кроме ActualTypeA * в TypeA *. Во всяком случае, я не в восторге от этого решения, потому что мой фактический интерфейс состоит из десятков тысяч строк кода на реализацию. И все A (), B (), C () и т. Д. Затрудняют чтение.
Кроме того, реализация bar () потребует некоторого дополнительного вуду:
inline std::vector<ActualTypeC*> &C(std::vector<TypeC*> &t)
{
return reinterpret_cast<std::vector<ActualTypeC*>&>(t);
}
TypeB *bar(std::vector<TypeC*> &c)
{
B(actualBar(C(c));
}
Еще один способ решить эту проблему — избежать каких-либо изменений на стороне реализации:
AbstractInterface.h:
class ActualTypeA;
class ActualTypeB;
class ActualTypeC;
namespace ImplemetationOne
{
using TypeA = ActualTypeA;
using TypeB = ActualTypeB;
using TypeC = ActualTypeC;
}
class OtherActualTypeA;
class OtherActualTypeB;
class OtherActualTypeC;
namespace ImplemetationTwo
{
using TypeA = OtherActualTypeA;
using TypeB = OtherActualTypeB;
using TypeC = OtherActualTypeC;
}
// Pre-define IMPLEMENTATION as ImplementationOne or ImplementationTwo
using TypeA = IMPLEMENTATION::TypeA;
using TypeB = IMPLEMENTATION::TypeB;
using TypeC = IMPLEMENTATION::TypeC;
TypeA *foo(TypeA *a, TypeB *b);
TypeB *bar(std::vector<TypeC*> &c);
TypeC *baz(TypeC *c, TypeA *c);
Это приводит к тому, что кто-то может случайно использовать специфичные для реализации типы вместо абстрактных. Кроме того, он требует определения IMPLEMENTATION для каждого модуля компиляции, который включает этот заголовок, и требует, чтобы они были согласованными. Я бы предпочел просто скомпилировать либоОтверждениеOne.cpp, либо ВнедрениеTwo.cpp, и все. Другим недостатком является то, что для дополнительных реализаций потребуется изменение заголовка, даже если мы не заинтересованы в типах, специфичных для реализации.
Это кажется очень распространенной проблемой, поэтому мне интересно, не хватает ли мне более элегантного и все же эффективного решения?
Вы можете использовать черты.
Следует минимальный рабочий пример:
#include<type_traits>
#include<vector>
struct ActualTypeA {};
struct ActualTypeB {};
struct ActualTypeC {};
struct OtherActualTypeA {};
struct OtherActualTypeB {};
struct OtherActualTypeC {};
enum class Lib { LibA, LibB };
template<Lib>
struct Traits;
template<>
struct Traits<Lib::LibA> {
using TypeA = ActualTypeA;
using TypeB = ActualTypeB;
using TypeC = ActualTypeC;
};
template<>
struct Traits<Lib::LibB> {
using TypeA = OtherActualTypeA;
using TypeB = OtherActualTypeB;
using TypeC = OtherActualTypeC;
};
template<Lib L>
struct Wrapper {
using LibTraits = Traits<L>;
static typename LibTraits::TypeA *foo(typename LibTraits::TypeA *a, typename LibTraits::TypeB *b) { return nullptr; }
static typename LibTraits::TypeB *bar(std::vector<typename LibTraits::TypeC*> &c) { return nullptr; }
static typename LibTraits::TypeC *baz(typename LibTraits::TypeC *c, typename LibTraits::TypeA *a) { return nullptr; }
};
int main() {
using MyWrapper = Wrapper<Lib::LibB>;
static_assert(std::is_same<decltype(MyWrapper::foo(nullptr, nullptr)), OtherActualTypeA*>::value, "!");
static_assert(std::is_same<decltype(MyWrapper::baz(nullptr, nullptr)), OtherActualTypeC*>::value, "!");
}
Не похоже, что C ++ поддерживает способ определения заранее объявленного класса как существующего класса. Так что я все равно использовал подход с приведениями (и вспомогательными функциями). Это стало 650-строчным изменением, но, по крайней мере, это гарантирует отсутствие дополнительных затрат.
Я думаю, что я предложу комитету по стандартам C ++ добавить для этого языковую функцию (или просто расслабить typedef / using, чтобы не вызывать ошибку переопределения), чтобы облегчить это в будущем …
Напишите две оболочки вокруг библиотек, которые реализуют одинаковые функции. Затем скомпилируйте и скомпонуйте только одну из этих реализаций. Есть небольшая загвоздка: чтобы получить реальные нулевые накладные расходы, вам нужно будет сделать только этот заголовок оболочки и перестроить весь проект при изменении реализации.
Вы можете либо использовать макросы для выбора реализации, либо вы можете заставить свою систему сборки компилировать только подмножество ваших исходных кодов.
«значение полиморфизма не может быть и речи»
Зачем? Я бы сказал, что наследование это именно то, что вы хотите … Что не так с полиморфизмом? Слишком медленно? Для чего? Это слишком медленно для того, что вы хотите? Или вы просто даете себе произвольное ограничение? Учитывая то, что я понял из вашего описания проблемы, полиморфизм именно то, что вы хотите! Вы хотите определить базовый класс B, который определяет контракт — набор методов. Вся ваша программа будет знать только этот базовый класс, никогда не ссылаясь на классы, производные от B. Затем вы реализуете 2 или более классов, производных от B — C и D -, которые фактически имеют код в своих методах, фактически что-то делают. Ваша программа будет знать только B, вызывая ее методы, не обращая внимания на то, является ли это кодом C или D, который действительно заставляет вещи происходить! Что ты имеешь против полиморфизма? Это один из краеугольных камней ООП, поэтому вы можете просто перестать использовать C ++ и придерживаться C …
«Я не могу позволить регрессии производительности для текущей реализации»
Конечно вы можете. Однако вы не можете позволить производительности ухудшаться тоже много. Вы точно знаете, сколько производительности вы потеряете с полиморфизмом? Вы потеряете производительность, но сколько именно? Вы пытались реализовать тестовую версию с надлежащим инструментарием, просто чтобы быть уверенными, что именно полиморфные вызовы виноваты в замедлении? Вы знаете, как часто совершаются эти полиморфные звонки? Иногда вы можете повысить производительность, не удаляя полиморфные вызовы — что является элегантным решением IMO для проблемы, которую он пытается решить, — но делая ваши интерфейсы менее разговорчивыми: кэширование результатов, пакетирование запросов и т. Д. Вы не будете первыми, кто попытается чтобы устранить очевидное решение S, потому что S на 700 мс медленнее, только чтобы найти S, должен был использоваться 6 раз в час …: S
Если все остальное терпит неудачу, у вас может быть две разные реализации одного и того же файла cpp, и ваш процесс сборки запускается дважды — по одному для каждой версии cpp.