什么是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提供了灵活构造类的方式
笔者水平有限,欢迎指正