高效优雅的接口抽象

Efficient and elegant interface abstraction

我正在尝试为两个具有相似功能的 third-party 库创建一个公共接口,以便我可以针对抽象接口和 select 在 compile-time 哪个实现进行编码使用。

我需要这个抽象接口不增加任何开销,这意味着多态性是不可能的。无论如何都不需要它,因为只有一个实际的实现在使用中。所以我最初的尝试是这样的:

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。所以接下来我尝试的是:

class TypeA : public ActualTypeA {};   // No more error
...
TypeA *foo(TypeA *a, TypeB *b)
{
    return actualFoo(a, b);   // Error
}

这里,actualFoo() returns一个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));
}

解决所有这些问题的另一种方法,避免需要任何 implementation-side 更改:

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-specific 类型而不是抽象类型。此外,它需要为包含此 header 的每个编译单元定义 IMPLEMENTATION,并要求它们保持一致。我宁愿只编译 ImplementationOne.cpp 或 ImplementationTwo.cpp 就是这样。另一个缺点是额外的实现需要修改 header,即使我们对 implementation-specific 类型没有实际兴趣。

这似乎是一个非常普遍的问题,所以我想知道我是否遗漏了任何更优雅且仍然有效的解决方案?

围绕实现完全相同功能的库编写两个包装器。然后编译 link 只有一个实现。有一个小问题:要获得真正的零开销,您必须只制作包装器 header 并在更改实现时重建整个项目。

您可以使用宏来 select 实现,也可以让您的构建系统只编译源代码的子集。

"meaning polymorphism is out of the question"

为什么?我会说继承正是你想要的......多态性有什么问题?太慢了?为了什么?它对你想要的东西来说太慢了吗?或者你只是给自己一个任意的约束?鉴于我从您的问题描述中了解到,多态性正是您想要的!你想定义一个基础 class B 来定义一个契约——一组方法。您的整个程序将只知道基数 class,从不引用从 B 派生的 classes。然后,您实现 2 个或更多从 B 派生的 classes – C 和 D – 即实际上在他们的方法中有代码,实际上做了一些事情。你的程序将只知道 B,调用它的方法而不关心是 C 还是 D 的代码真正让事情发生了!无论如何,你有什么反对多态性的?它是 OOP 的基石之一,因此您可能只是停止使用 C++ 并坚持使用 C...

你可以使用特征。
它遵循一个最小的工作示例:

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

"I can't allow performance to regress for the current implementation"

当然可以。但是,您不能让性能下降太多。您是否确切知道多态性会损失多少性能?你会失去性能,但究竟是多少%?您是否尝试过使用适当的工具实现测试版本,以确保是多态调用导致了速度下降?您知道这些多态调用的频率有多高吗?有时你可以提高性能不是通过删除多态调用——这是 IMO 对它试图解决的问题的一个优雅的解决方案——而是通过让你的接口不那么繁琐:缓存结果、捆绑请求等。你不会是第一个尝试的消除一个明显的解决方案 S,因为 S 慢了 700 毫秒,结果发现 S 每小时要使用 6 次...:S

如果一切都失败了,你可以对同一个 cpp 文件有两个不同的实现,并让你的构建过程 运行 两次 - 每个 cpp 版本一次。

C++ 似乎不支持将前向声明的 class 定义为现有 class 的方法。所以我最终还是使用了强制转换(和辅助函数)的方法。改成了650行,但至少保证不增加任何开销

我想我会建议 C++ 标准委员会为此添加一个语言特性(或者简单地放宽 typedef/using 以不产生重新定义错误)以便将来更容易...