高效优雅的接口抽象
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 以不产生重新定义错误)以便将来更容易...
我正在尝试为两个具有相似功能的 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 以不产生重新定义错误)以便将来更容易...