高效优雅的界面抽象

时间:2016-09-23 18:34:20

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

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

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

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而已。另一个缺点是,即使我们对特定于实现的类型没有实际兴趣,其他实现也需要修改标题。

这似乎是一个非常常见的问题,所以我想知道我是否错过了任何更优雅且效率更高的解决方案?

5 个答案:

答案 0 :(得分:0)

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

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

答案 1 :(得分:0)

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

我想我会建议C ++标准委员会为此添加一个语言功能(或者只是放松typedef / using以不产生重新定义错误),以便将来更容易...

答案 2 :(得分:-1)

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

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

答案 3 :(得分:-1)

“意味着多态性是不可能的”

为什么呢?我会说继承正是你想要的......多态性有什么问题?太慢了?为了什么?对你想要的东西来说太慢了吗?或者你只是给自己一个任意的约束?鉴于我从你的问题描述中理解,polimorphism就是你想要的!您想要定义一个定义合同的基类B - 一组方法。你的整个程序只知道那个基类,从不引用从B派生的类。然后,你实现了两个或更多从B - C和D派生的类 - 它们实际上在他们的方法中有代码,实际上做了一些事情。你的程序只会知道B,调用它的方法而不关心C或D的代码是否实际让事情发生!无论如何,你对多态性有什么看法?它是OOP的基石之一,所以你可能会停止使用C ++并坚持使用C ...

答案 4 :(得分:-1)

“我不能让当前实施的性能回归”

当然可以。但是,您不能让性能退回太多。您是否确切知道多态性会丢失多少性能?你会失去性能,但究竟有多少%?您是否尝试使用适当的仪器实现测试版本,以确保这是因为减速而应该归咎于的多态调用?您是否知道这些多态调用的频率是多少?有时你可以通过删除多态调用来提高性能 - 这是一个优雅的解决方案IMO,它试图解决它的问题 - 但是通过使你的接口不那么繁琐:缓存结果,捆绑请求等等。你不会是第一个尝试消除一个明显的解决方案S因为S慢700毫秒,只发现S每小时使用6次......:S

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