CRTP有什么用?

什么是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提供了灵活构造类的方式

 

  笔者水平有限,欢迎指正

 

 

 

 

 

 

 

posted @ 2013-04-03 11:51  mightofcode  阅读(3140)  评论(1编辑  收藏  举报