c++接口与模板
C++ Interface vs Template
对于同样的问题,我有两个解决方案-从一个"控制器"到使用的对象进行某种回调,我不知道该选择什么。
方案一:使用接口
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关系?那么OO方法可能会起作用。它们是无关的吗?也就是说,它们是否有一些共同的特征,但没有直接的is-a关系可以建模?使用模板方法。
模板的主要优点是,您可以使用不符合特定和精确继承层次结构的类型。例如,你可以在一个可复制构造的向量中存储任何东西(在c++ 11中是可移动构造的),但是int
和Car
在任何方面都没有真正的联系。这样,您就减少了使用UseA
类型的不同类型之间的耦合。
模板的缺点之一是,每个模板实例化都是不同的类型,与从同一基本模板生成的其余模板实例化无关。这意味着您不能将UseA<A>
和UseA<B>
存储在同一个容器中,这会导致代码膨胀 (UseA<int>::foo
和UseA<double>::foo
都是在二进制文件中生成的),更长的编译时间(即使不考虑额外的函数,使用UseA<int>::foo
的两个翻译单元都将生成相同的函数,并且链接器将不得不丢弃其中一个)。
关于其他答案所声称的性能,他们在某种程度上是正确的,但大多数人都错过了重要的一点。与动态分派相比,选择模板的主要优点不是动态分派的额外开销,而是编译器可以内联小函数(如果函数定义本身是可见的)。
如果函数不是内联的,除非函数只需要很少的周期来执行,否则函数的总成本将超过动态调度的额外成本(即调用中的额外间接和在多重/虚拟继承的情况下this
指针的可能偏移)。如果函数做一些实际的工作,并且/或者它们不能内联,它们将具有相同的性能。
即使在一种方法与另一种方法的性能差异可以测量的少数情况下(假设函数只需要两个周期,并且调度因此使每个函数的成本翻倍),如果该代码是80%的代码的一部分,占用的cpu时间少于20%,假设这段特定的代码占用了1%的CPU(这是一个巨大的数字,如果你考虑到为了获得显著的性能,函数本身必须只占用一两个周期!),那么你谈论的是1小时程序运行中的30秒。再次检查前提,在2GHz的cpu上,1%的时间意味着该函数必须每秒调用超过1000万次。
以上所有都是不确定的,它与其他答案的方向相反(即,有些不精确的地方可能会使差异看起来比实际情况小,但实际情况更接近于此,而不是一般答案动态调度会使您的代码变慢
各有利弊。来自c++编程语言:
- 当运行时效率更高时,选择模板而不是派生类。
- 如果添加新的变体而不重新编译是很重要的,则选择派生类而不是模板。
- 当不能定义公共基时,首选模板而不是派生类。
- 当具有兼容性约束的内置类型和结构很重要时,首选模板而不是派生类。
然而,模板有它们的缺点
使用面向对象接口的代码可以隐藏在。cpp/中。CC文件,当模板强制在头文件中公开整个代码时;
- 模板会导致代码膨胀;
- OO接口是显式的,只要对模板参数的需求是隐式的,并且只存在于开发人员的头脑中;
大量使用模板会影响编译速度。
使用哪一个取决于你的情况和你的喜好。模板化的代码可能会产生一些笨拙的编译错误,从而导致诸如STL错误解密之类的工具。希望这些概念很快就能实现。
模板的情况会有稍好的性能,因为没有涉及到虚调用。如果回调非常频繁地使用,那么最好使用模板解决方案。请注意,"极其频繁"直到每秒数千次才会真正生效,甚至可能更晚。
另一方面,模板必须在头文件中,这意味着对它的每次更改都将强制重新编译所有调用它的站点,不像在接口场景中,实现可能在.cpp中,并且是唯一需要重新编译的文件。
你可以把接口看作一个契约。从该接口派生的任何类都必须实现该接口的方法。
另一方面,模板隐式地具有一些约束。例如,T
模板参数必须有一个方法f
。这些隐含的需求应该被仔细地记录下来,涉及模板的错误消息可能会很混乱。
Boost Concept可以用于概念检查,这使得隐式模板需求更容易理解。
您描述的选择是静态多态性与动态多态性之间的选择。如果你搜索它,你会发现很多关于这个话题的讨论。
对于这样一个笼统的问题,很难给出一个具体的答案。一般来说,静态多态可以提供更好的性能,但是由于c++ 11标准中缺少概念,这也意味着当一个类没有建模所需的概念时,您可能会得到有趣的编译器错误消息。我会使用模板版本。如果你从性能的角度考虑这一点,那么它是有意义的。
Virtual Interface——使用Virtual意味着方法的内存是动态的,在运行时决定。这有开销,因为它必须查询vlookup表以在内存中找到该方法。
Templates——你得到静态映射。这意味着当你的方法被调用时,它不需要查阅查找表,并且已经知道该方法在内存中的位置。
如果你对性能感兴趣,那么模板几乎总是你的选择。
选项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
没有共同的基接口类。
- C++核心准则 C35 对于接口类"A base class destructor should be either public and virtual, or protected and nonv
- Visual C++GC接口如何启用它以及要包含哪个库
- Windows.h与GLFW.h的接口
- 当字段可以为null时,如何使用C++接口在Avro中写入数据
- 提供与TMP和SFINAE的通用接口
- 为重写std::exception的库生成swig接口时出错
- 内联如何影响模块接口中的成员函数
- COM 接口 c# 封送数组数组
- 如何在 SCIP C++ 接口中获取 MILP 约束矩阵中的系数值
- 重载 -> shared_ptr 个实例中的箭头运算符<interface>,接口中没有纯虚拟析构函数
- 如何绑定 C++ gRPC 客户端的网络接口
- 模板化接口 - 创建一个泛型模板类以返回任何容器
- 如何从实现接口的模板化类实例访问结构
- 带有进度表的 curl 多接口程序
- 设计帮助 - 为不同类型的消息处理通用接口的设计模式
- 我可以在具有一个标头和一个接口的 cpp 文件中有多个嵌入吗?
- 类接口,可以创建N个方法
- 类具有相同的接口,但参数的类型不同
- 如何与 Cheerp/js 中的 extern 变量接口?
- 如何使用现代 CMake 安装捆绑的接口依赖项?