[英]C++ Interface vs Template
对于同一问题,我有 2 个解决方案 - 从一个“控制器”到使用的对象进行某种回调,但我不知道该选择什么。
方案一:使用接口
struct AInterface
{
virtual void f() = 0;
};
struct A : public AInterface
{
void f(){std::cout<<"A::f()"<<std::endl;}
};
struct UseAInterface
{
UseAInterface(AInterface* a) : _a(a){}
void f(){_a->f();}
AInterface* _a;
};
解决方案 2:使用模板
struct A
{
void f(){std::cout<<"A::f()"<<std::endl;}
};
template<class T>
struct UseA
{
UseA(T* a) : _a(a){}
void f(){_a->f();}
T* _a;
};
这只是一个简单的示例来说明我的问题。 在现实世界中,接口将具有多个功能,一个类可能(并且将!)实现多个接口。
该代码不会用作外部项目的库,我不必隐藏模板实现 - 我这样说是因为如果我需要隐藏“控制器”实现,第一种情况会更好。
你能告诉我每种情况的优点/缺点以及什么更好用吗?
在我看来,性能应该被忽略(不是真的,但微优化应该),直到你有理由这样做。 如果没有一些硬性要求(这是一个占用大部分 CPU 的紧密循环,接口成员函数的实际实现非常小......)如果不是不可能注意到差异,那将是非常困难的。
所以我会专注于更高的设计水平。 UseA
使用的所有类型共享一个公共基础是否有意义? 他们真的有关系吗? 类型之间是否有明确的is-a关系? 那么面向对象的方法可能会奏效。 他们没有关系吗? 也就是说,它们是否共享某些特征,但没有可以建模的直接is-a关系? 使用模板方法。
模板的主要优点是您可以使用不符合特定和确切继承层次结构的类型。 例如,您可以将任何内容存储在可复制构造(在 C++11 中可移动构造)的向量中,但int
和Car
在任何方面都没有真正相关。 这样,您可以减少与UseA
类型一起使用的不同类型之间的耦合。
模板的一个缺点是每个模板实例化都是一种不同的类型,与从同一基本模板生成的其余模板实例化无关。 这意味着您不能将UseA<A>
和UseA<B>
存储在同一个容器中,会出现代码膨胀( UseA<int>::foo
和UseA<double>::foo
都是在二进制文件中生成的),更长的编译时间(即使不考虑额外的函数,使用UseA<int>::foo
两个翻译单元都将生成相同的函数,链接器将不得不丢弃其中一个)。
关于其他答案声称的性能,它们在某种程度上是正确的,但大多数都没有抓住要点。 选择模板而不是动态分派的主要优点不是动态分派的额外开销,而是编译器可以内联小函数的事实(如果函数定义本身是可见的)。
如果函数没有内联,除非函数只需要很少的周期来执行,函数的整体成本将胜过动态分派的额外成本(即调用中的额外间接性和this
指针在调用中可能的偏移量)多重/虚拟继承的情况)。 如果函数做了一些实际工作,和/或它们不能内联,它们将具有相同的性能。
即使在少数情况下,如果此代码是 80%占用 cpu 时间不到 20% 的代码,并说这段特定的代码占用了 cpu 的 1%(如果您考虑到为了使性能引人注目,函数本身必须只占用一两个周期!)那么你说的是 1 小时程序运行中的 30 秒。 再次检查前提,在 2GHz cpu 上,1% 的时间意味着该函数必须每秒调用超过 1000 万次。
以上所有都是挥手,它与其他答案的方向相反(即存在一些不精确性可能会使差异看起来比实际小,但现实比实际情况更接近于此到一般答案动态调度会使您的代码变慢。
各有利弊。 来自C++ 编程语言:
- 当运行时效率非常重要时,更喜欢模板而不是派生类。
- 如果在不重新编译的情况下添加新变体很重要,则首选派生类而不是模板。
- 当无法定义公共基类时,优先使用模板而不是派生类。
- 当具有兼容性约束的内置类型和结构很重要时,优先使用模板而不是派生类。
但是,模板有其缺点
- 使用 OO 接口的代码可以隐藏在 .cpp/.CC 文件中,只要模板强制在头文件中公开整个代码;
- 模板会导致代码膨胀;
- OO 接口是显式的,只要对模板参数的要求是隐式的并且只存在于开发人员的头脑中;
- 大量使用模板会影响编译速度。
使用哪种取决于您的情况以及您的偏好。 模板化代码会产生一些迟钝的编译错误,这导致了诸如STL 错误解密之类的工具。 希望,概念将很快实施。
模板案例的性能会稍微好一些,因为不涉及虚拟调用。 如果回调使用非常频繁,请支持模板解决方案。 请注意,“非常频繁”直到每秒数千次才真正开始,甚至可能更晚。
另一方面,模板必须在头文件中,这意味着对它的每次更改都将强制重新编译所有调用它的站点,这与接口方案不同,在接口方案中,实现可以在 .cpp 中并且是唯一需要的文件重新编译。
您可以考虑像合同这样的接口。 从它派生的任何类都必须实现接口的方法。
另一方面,模板隐含地具有一些约束。 例如,您的T
模板参数必须有一个方法f
。 这些隐含的要求应该仔细记录,涉及模板的错误消息可能会非常混乱。
Boost Concept可用于概念检查,这使得隐式模板需求更容易理解。
您描述的选择是静态多态与动态多态之间的选择。 如果你搜索这个话题,你会发现很多关于这个话题的讨论。
对于这样一个普遍的问题,很难给出具体的答案。 一般来说,静态多态可能会给你更好的性能,但是 C++11 标准中缺少概念也意味着当一个类没有对所需的概念建模时,你可能会得到有趣的编译器错误消息。
我会选择模板版本。 如果您从性能的角度考虑这一点,那么这是有道理的。
虚拟接口 - 使用虚拟意味着该方法的内存是动态的,并在运行时决定。 这具有开销,因为它必须查阅 vlookup 表才能在内存中定位该方法。
模板 - 您获得静态映射。 这意味着当您的方法被调用时,它不必查阅查找表并且已经知道该方法在内存中的位置。
如果您对性能感兴趣,那么模板几乎总是可以使用的选择。
选项3怎么样?
template<auto* operation, class Sig = void()>
struct can_do;
template<auto* operation, class R, class...Args>
struct can_do<operation, R(Args...)> {
void* pstate = 0;
R(*poperation)(void*, Args&&...) = 0;
template<class T,
std::enable_if_t<std::is_convertible_v<
std::invoke_result_t<decltype(*operation), T&&, Args&&...>,
R>,
bool> = true,
std::enable_if_t<!std::is_same_v<can_do, std::decay_t<T>>, bool> =true
>
can_do(T&& t):
pstate((void*)std::addressof(t)),
poperation(+[](void* pstate, Args&&...args)->R {
return (*operation)( std::forward<T>(*static_cast<std::remove_reference_t<T>*>(pstate)), std::forward<Args>(args)... );
})
{}
can_do(can_do const&)=default;
can_do(can_do&&)=default;
can_do& operator=(can_do const&)=default;
can_do& operator=(can_do&&)=default;
~can_do()=default;
auto operator->*( decltype(operation) ) const {
return [this](auto&&...args)->R {
return poperation( pstate, decltype(args)(args)... );
};
}
};
现在你可以做
auto invoke_f = [](auto&& elem)->void { elem.f(); };
struct UseA
{
UseA(can_do<&invoke_f> a) : m_a(a){}
void f(){(m_a->*&invoke_f)();}
can_do<&invoke_f> m_a;
};
测试代码:
struct A {
void f() { std::cout << "hello world"; }
};
struct A2 {
void f() { std::cout << "goodbye"; }
};
A a;
UseA b(a);
b.f();
A2 a2;
UseA b2(a2);
b2.f();
在can_do
上拥有更丰富的多操作界面作为练习。
UseA
不是模板。 A
和A2
没有共同的基接口类。
然而它有效。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.