【问题标题】:Efficient and elegant interface abstraction高效优雅的接口抽象
【发布时间】:2016-09-23 18:34:20
【问题描述】:

我正在尝试为两个具有相似功能的第三方库创建一个通用接口,以便我可以针对抽象接口进行编码并在编译时选择要使用的实现。

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

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,并要求它们保持一致。我宁愿只编译 ImplementationOne.cpp 或 ImplementationTwo.cpp,就是这样。另一个缺点是额外的实现需要修改标头,即使我们对特定于实现的类型没有真正的兴趣。

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

【问题讨论】:

  • 您似乎正在努力解决继承本应解决的问题。由于您正在寻找编译时接口,因此您需要查看模板。
  • 调用一层额外的函数是开销,所以它不能解决你的问题。简而言之,我怀疑您在没有分析或确定性的情况下选择了一些随机的东西作为“不要这样做它有成本”,而另一些则是“确定这样做它没有成本”。这两个接口真的是method for method,function for function,type for type bijective吗?
  • 如果选择实现的信息可用,您可以在编译时使用 tag-dispatch 或 SFINAE 来选择实现。它是编译时多态性,但我想你只是想避免运行时多态性。
  • @Yakk 不,我目前用于实现接口的库不能完美地映射到它。基本上我在图书馆的顶部有一个薄包装。该层的成本是合理的。但现在我想使用一个备用库来实现相同的接口(使用抽象类型)。为这种抽象增加开销是不合理的,因为实现是在编译时选择的。

标签: c++ abstraction forward-declaration type-alias


【解决方案1】:

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

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

【讨论】:

  • 这似乎与使用 IMPLEMENTATION 宏的解决方案存在相同的问题。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • 您的解决方案似乎只是重复了实际问题。我们如何编写两个实现相同接口的包装器?最明显的答案似乎是使用演员表,但这不是很优雅。有没有办法转发声明一个类型,然后将其定义为与现有类型相同?
    • 我的解决方案是编写两个实现,但只编译其中一个。您可以使用构建系统跳过当前不需要的文件,或者将实现完全放在 #ifdef end 下,只保留其中一个。
    • 换句话说:如果您想使用不同的后端库构建应用程序的两个变体,那么它是构建系统的工作,而不是语言技巧。
    【解决方案4】:

    “意义多态性是不可能的”

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

    【讨论】:

    • 这是一个已经被多个客户端使用的高性能框架。当我抽象接口以允许第二个实现时,我不能允许当前实现的性能倒退。此外,这些实现使用了我无法控制的第三方库,因此我无法从公共基类派生它们的类。我可以包装它们,但后来我又需要强制转换来上转换它们。
    【解决方案5】:

    “我不能让当前实现的性能倒退”

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

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

    【讨论】:

    • “计算机科学中的所有问题都可以通过另一个层次的间接来解决……除了间接层太多的问题。”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-21
    • 1970-01-01
    • 2011-02-27
    • 2011-05-22
    • 2010-10-12
    • 1970-01-01
    • 2021-07-06
    相关资源
    最近更新 更多