什么是CRTP?
The curiously recurring template pattern (CRTP) is a C++ idiom in which a class X
derives from a class template instantiation using X
itself as template argument.
类X继承了一个以X作为模板参数的的模板,这就是CRTP,具体介绍请参看维基百科
CRTP简介
CRTP的意义是父类(接下来我们称之为CRTP父类,相应的子类成为CRTP子类)知道子类的类型,可以做一些虚函数做不到的事,比如维基百科里面提到的类计数,clone函数
这两个东西用虚函数做起来都不甚方便,本文的目的是探讨CRTP的通用场景
CRTP可以代替虚函数吗
不可以,虚函数实现了动态多态,也就是让基类指针指向子类,CRTP做不到这一点
根据维基,CRTP实现了"静态多态",关于"静态多态",我的理解是在编译时就根据基类指针转换为子类指针,达到类似多态的效果,但本质上跟动态多态还是两码事
所谓"静态多态"似乎不符合多态的定义:CRTP产生了不同的父类,所以不存在同样的基类指针指向不同子类这样的情况
或许"静态多态"应该换一个更直观的名字
那么CRTP有什么用呢?
在网上看了很多文章,大多语焉不详,举的例子大部分是类计数器,单例模式等看起来很trick,不实用的东西,不可否认这些东西确实"有用",但是你真的会用在在自己的项目中吗?
笔者以前写代码的时候也遇到过基类需要知道子类类型的情况,那个时候很自然的时候就用了CRTP,当时我并不知道这是CRTP,但是CRTP有没有普适性更强的用法呢?
包括荣毅的一篇文章http://wenku.baidu.com/view/a14844a1b0717fd5360cdcb2.html,也没有举实际的例子,看不懂
看看WTL
但是幸好有WTL,WTL中大量使用了CRTP,我们以CDoubleBufferImpl来看看WTL是如何运用CRTL的
CDoubleBufferImpl看名字猜测一个双缓冲的渲染封装,核心函数是OnPaint,DoPaint
CDoubleBufferImpl的定义:
template <class T>
class CDoubleBufferImpl
{
public:
// Overrideables
void DoPaint(CDCHandle /*dc*/) //子类需要覆盖此函数,没有这个函数也行,但是会出现天书般的模板编译信息
{
// must be implemented in a derived class
ATLASSERT(FALSE);
}
// Message map and handlers
BEGIN_MSG_MAP(CDoubleBufferImpl)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
#ifndef _WIN32_WCE
MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint)
#endif // !_WIN32_WCE
END_MSG_MAP()
LRESULT OnEraseBackground(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
return 1; // no background painting needed
}
LRESULT OnPaint(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
T* pT = static_cast<T*>(this); //典型CRTP代码,转换为子类指针
ATLASSERT(::IsWindow(pT->m_hWnd)); //1
if(wParam != NULL)
{
RECT rect = { 0 };
pT->GetClientRect(&rect); //2
CMemoryDC dcMem((HDC)wParam, rect);
pT->DoPaint(dcMem.m_hDC); //3
}
else
{
CPaintDC dc(pT->m_hWnd); //4
CMemoryDC dcMem(dc.m_hDC, dc.m_ps.rcPaint);
pT->DoPaint(dcMem.m_hDC); //5
}
return 0;
}
};
编号1,2,3,4,5表明子类需要实现的接口
实现一个双缓冲窗口的典型代码:
class TCtrl:
public CWindowImpl< TCtrl>,
public WTL::CDoubleBufferImpl<TCtrl> // 继承双缓冲类
这样TCtrl从CWindowimpl获得了窗口的行为,从CDoubleBufferWindowImpl获得了双缓冲的行为,从而得到了一个双缓冲窗口
MSG_MAP妨碍了我们的分析,我们抛开MSG_MAP,简化TCtrl的工作流程:
CWindowimpl接收到PAINT消息,这个消息又发给了CDoubleBufferImpl,CDoubleBufferImpl进行一些处理然后调用TCtrl的DoPaint完成绘制
在这里WTL使用多重继承+CRTP来拓展类的行为,而不是组合或者单继承
在这里使用CRTP有一个明显的好处:可以少写很多琐碎的代码,CDoubleBufferImpl模板知道子类的的类型,可以直接使用子类的接口
不用CRTP如何拓展类?
如果使用组合:
我们需要定义一个CDoubleBufferImpl类,这个类实现了双缓冲,注意它用到了GetClientRect之类的东西,所以我们的TCtrl需要把这些数据push到CDoubleBufferImpl,或者定义一些接口让CDoubleBufferImpl使用,
然后我们调用CDoubleBufferImpl类完成工作
如果使用单继承:
TCtrl需要重写一部分CWindowimpl的方法,在这些方法中实现双缓冲
组合的方法要写很多代码,虽然让CDoubleBufferImpl和CWindowimpl解耦,但是写这么多代码增加了很多复杂度
单继承的方法看起来非常不错似乎比多继承+CRTP还要简单,但是这样就把CWindowimpl和双缓冲的实现耦合起来了,如果我现在需要给CWindowimpl增加另一种特性,比如"ReSize",为了满足可变的需求,我们需要把库
设计得足够全面那么,我肯定需要把ReSize和双缓冲两个属性进行组合,这样就会产生4个类,而且会有重复代码(这个时候你肯定会想用组合来实现),如果我再想为添加另一种行为呢?结果是越来越多的类,代码很快就难以维护了
但是多继承+CRTP提供了另一种方式
使用多继承+CRTP比组合的代码少,比单继承易于拓展,要添加行为,只需要继承一个类就好了,多个行为相互组合?再继承几个
多继承+CRTP也有缺点:
1,使用模板,牵一发而动全身,改动模板会引起大量重编,做过大型c++项目的都明白这实在是一个难以忍受的过程,笔者所在的项目曾经有一个用得比较多的模板类(其实这个类完全没有必要使用模板),笔者有一段时间需要去改动
这个2000行的庞然大物,每一次改动都要编译几十分钟,苦不堪言
2,代码比较难读,特别是对于新手,这变相增加了维护成本
3,多继承+CRTP很灵活,但是封装性不如组合
总结:
1 CRTP可以用在任何基类需要子类类型的场合
2 多继承+CRTP提供了灵活构造类的方式
笔者水平有限,欢迎指正
原文链接: https://www.cnblogs.com/mightofcode/archive/2013/04/03/2996323.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/83203
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!