我试图为两个具有类似功能的第三方库创建一个通用接口,以便我可以针对抽象接口进行编码,并在编译时选择要使用的实现。
我需要这个抽象接口不添加任何开销,这意味着多态性是不可能的。无论如何都不应该需要它,因为只有一个实际的实现在使用中。所以我最初的尝试看起来像这样:
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而已。另一个缺点是,即使我们对特定于实现的类型没有实际兴趣,其他实现也需要修改标题。
这似乎是一个非常常见的问题,所以我想知道我是否错过了任何更优雅且效率更高的解决方案?
答案 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版本一次。