Эффективный и элегантный интерфейс абстракции

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

Мне нужен этот абстрактный интерфейс, чтобы не добавлять никаких накладных расходов, то есть о полиморфизме не может быть и речи. В любом случае это не нужно, так как используется только одна фактическая реализация. Итак, моя первоначальная попытка выглядела так:

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, и все. Другим недостатком является то, что для дополнительных реализаций потребуется изменение заголовка, даже если мы не заинтересованы в типах, специфичных для реализации.

Это кажется очень распространенной проблемой, поэтому мне интересно, не хватает ли мне более элегантного и все же эффективного решения?

2

Решение

Вы можете использовать черты.
Следует минимальный рабочий пример:

#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, "!");
}
0

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

Не похоже, что C ++ поддерживает способ определения заранее объявленного класса как существующего класса. Так что я все равно использовал подход с приведениями (и вспомогательными функциями). Это стало 650-строчным изменением, но, по крайней мере, это гарантирует отсутствие дополнительных затрат.

Я думаю, что я предложу комитету по стандартам C ++ добавить для этого языковую функцию (или просто расслабить typedef / using, чтобы не вызывать ошибку переопределения), чтобы облегчить это в будущем …

0

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

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

-1

«значение полиморфизма не может быть и речи»

Зачем? Я бы сказал, что наследование это именно то, что вы хотите … Что не так с полиморфизмом? Слишком медленно? Для чего? Это слишком медленно для того, что вы хотите? Или вы просто даете себе произвольное ограничение? Учитывая то, что я понял из вашего описания проблемы, полиморфизм именно то, что вы хотите! Вы хотите определить базовый класс B, который определяет контракт — набор методов. Вся ваша программа будет знать только этот базовый класс, никогда не ссылаясь на классы, производные от B. Затем вы реализуете 2 или более классов, производных от B — C и D -, которые фактически имеют код в своих методах, фактически что-то делают. Ваша программа будет знать только B, вызывая ее методы, не обращая внимания на то, является ли это кодом C или D, который действительно заставляет вещи происходить! Что ты имеешь против полиморфизма? Это один из краеугольных камней ООП, поэтому вы можете просто перестать использовать C ++ и придерживаться C …

-1

«Я не могу позволить регрессии производительности для текущей реализации»

Конечно вы можете. Однако вы не можете позволить производительности ухудшаться тоже много. Вы точно знаете, сколько производительности вы потеряете с полиморфизмом? Вы потеряете производительность, но сколько именно? Вы пытались реализовать тестовую версию с надлежащим инструментарием, просто чтобы быть уверенными, что именно полиморфные вызовы виноваты в замедлении? Вы знаете, как часто совершаются эти полиморфные звонки? Иногда вы можете повысить производительность, не удаляя полиморфные вызовы — что является элегантным решением IMO для проблемы, которую он пытается решить, — но делая ваши интерфейсы менее разговорчивыми: кэширование результатов, пакетирование запросов и т. Д. Вы не будете первыми, кто попытается чтобы устранить очевидное решение S, потому что S на 700 мс медленнее, только чтобы найти S, должен был использоваться 6 раз в час …: S

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

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