首页 新闻 会员 周边 捐助

关于c++ template多态——CRTP 模式

0
悬赏园豆:50 [已解决问题] 解决于 2012-11-28 14:32

看了很多有关CRTP(Curiously Recurring Template Prattern)的介绍,知道CRTP是什么,但不知道究竟应该在什么情况下用,请高手回答。

为了便于说明,以下给出三种类的继承方式:

第一种,普通继承,没有虚函数,子类同名函数完全覆盖父类:

struct Base

{

void Func() { std::cout << " base's func" << std::endl; }

};

struct Drv : public Base

{

void Func() { std::cout << " drv's func" << std::endl; }

};

第二种,普通继承,用虚函数:

struct Base

{

virtual void Func() { std::cout << " base's func" << std::endl; }

};

struct Drv : public Base

{

void Func() { std::cout << " drv's func" << std::endl; }

};

第三种:采用CRTP模式:

template <typename Derived>

struct Base

{

void Func() { static_cast<Derived&>(*this).FuncImpl(); }

void FuncImpl() { std::cout << "base's func" << std::endl; }

};

struct Drv : public Base<Drv>

{

void FuncImpl() { std::cout << "drv's func" << std::endl; }

};

我想问的是,为什么需要第三种方式???在我看来第三种和第一种比并没有多大优势。

第一种方式实际仅仅实现了子类对父类的共享,即公共的操作写在父类,子类仅仅实现个性的细节。

第二种方式除实现共享外,可以用父类的指针或引用实现多态,即:

Base* p = new Drv; p->Func();

甚至可以将父类的指针搜集到容器,之后可以进行无区别地批量处理(假设Drv1, Drv2, Drv3...均继承Base):

/// 搜集

std::vector<Base* p> container;

container.push_back(new Drv1);

container.push_back(new Drv2);

container.push_back(new Drv3);

...

/// 批量处理

for (unsigned int i = 0; i < container.size(); ++i) {

container[i]->Func();

}

第三种方式没有虚函数,的确可以省去动态绑定所需的开销,而且能够用于虚函数无法应用的地方,如内联,或函数模板。但它与第一种方式的区别何在?

虽然也可以用父类指针实现多态:Base<Drv>* p = new Drv; p->Func();

但由于父类必须显式指定子类作为模板参数,从应用的角度说它的优势体现在哪?这和如下写法有何区别: Drv* p = new Drv; p->Func(); (用第一种方式就可以实现)

另外,采用CRTP能将父类指针存放于容器中,然后进行无区别地处理吗(如第二种方式一样)??

如果以上两点都做不到,我不知道为什么还要采用CRTP,而不直接用第一种方式??它的应用点到底在哪???或者能做到,如何做?

我模板只是初步了解,还请高手指教,非常感谢!!!!

didigene的主页 didigene | 初学一级 | 园豆:157
提问于:2012-11-23 15:22
< >
分享
最佳答案
0

第一种方式是动态绑定,第三种方式是静态绑定。后者在性能更好。

收获园豆:45
Launcher | 高人七级 |园豆:45050 | 2012-11-23 16:14

感谢你的问答,不过,第一种方式是动态绑定吗?因为没有虚函数,所以在编译期就能确定了,不是吗?

didigene | 园豆:157 (初学一级) | 2012-11-23 16:48

@didigene: 看错了,第一种方式没法实现多态,动态绑定和静态绑定都是针对多态的。因此第一种方式和后面两种方式不在同一个比较基线上。

Launcher | 园豆:45050 (高人七级) | 2012-11-23 16:57

你好,我想知道第一种方式为什么不能替代第三种,有什么例子能说明第三种方式要优于第一种吗,非常感谢!

didigene | 园豆:157 (初学一级) | 2012-11-27 13:35

@didigene: 因为第一种方式实现不了多态。关于如何使用模板编程,你可以自学下,会发现很多精妙之处。第三种方式还有个优于第二种方式的方面:类型安全。我用WTL中一种最常的用法来演示(只为了说明问题,不是完整实现):

template <class T>
class CDoubleBufferImpl

{

public:

   void DoPaint(CDCHandle /*dc*/){ // 派生类实现此方法}

   LRESULT OnPaint(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)

   {

      T* pT = static_cast<T*>(this);

      pT->DoPaint();  // 调用派生类的 DoPaint 方法

    }

  
};

Launcher | 园豆:45050 (高人七级) | 2012-11-27 14:12

@Launcher: 感谢您的及时回复,只就您给的这个例子而言,如果把模板去掉:

class CDoubleBufferImpl
{
public:
        void DoPaint(CDCHandle dc) {} //派生类重定义
        LRESULT OnPaint(UINT uMsg, ...) {
                DoPaint();  //调用被派生类覆盖的DoPaint()
        }
};

我想比较一下这两种方法。我写的的确不能用父类的指针或引用实现多态。但即使用模板实现这个功能,父类的类型必须含有派生类的类型,这样的多态的意义在哪,如

CDoubleBufferImpl<MyCDoubleBufferImpl>* myImpl = new MyCDoubleBufferImpl;

myImpl->OnPaint(...);

与其这样写,为何不直接写成

MyCDoubleBufferImpl* myImpl = new MyCDoubleBufferImpl;
myImpl->OnPaint(...);

我想CRTP的优势应该不在这里,您有提到提到类型安全,能否详细说明一下?

我后来查了一下,发现CRTP的确有非模板无法实现的功能:一个是在父类中写一个方法,其中涉及子类的拷贝;另一个是在父类中定义一个静态变量,并限制这个静态变量只在同一个派生类的对象之间共享。

例1:

template <typename Derived>
class Base
{
public:
    Derived* CopyMyself() {
        // 调用派生类Derived的拷贝构造函数
        return new Derived(static_cast<Derived&>(*this));
    }

    // 与另一个Derived对象交换
    void Swap(Derived& drv) {
        Derived temp = drv;
        drv = static_cast<Derived&>(*this);
        static_cast<Derived&>(*this) = temp;
    }
};

 

如果不用模板,这两个函数必须在每一个派生类中都写一遍。

例2. 统计实例化对象的个数

template <typename Derived>
class Countable
{
protected:
    Countable() {
        ++Countable<Derived>::count_;
    }

    Countable(const Countable<Derived>&) {
        ++Countable<Derived>::count_;
    }

    ~Countable() {
        --Countable<Derived>::count_;
    }

public:
    static int GetCount()  {
        return Countable<Derived>::count_;
    }

private:
    static int count_;
}

template <typename Derived>
int Countable<Derived>::count_ = 0;

(漏了个分号,不改了)

这是我知道的CRTP的两种用法,如果您还能想到别的可以告诉我。

谢谢!

didigene | 园豆:157 (初学一级) | 2012-11-28 10:33

@Launcher: 今上午我回复了你上面的内容,谈了我的几点想法,想让你看看的,但回复的内容不见了,很奇怪。

didigene | 园豆:157 (初学一级) | 2012-11-28 13:13

@didigene: 如果把模板参数去掉,你就无法实现多态,也就是是说基类的OnPaint不会调用派生类的DoPaint方法。那么你每个派生类都要自己重新去写OnPaint方法,而我们在OnPaint方法中需要将派生类重复的代码提炼到基类(这里的示例是为每个派生的窗体类实现Double-Buffer)。

类设计的一个重要原则是消除重复代码,达到代码复用

请你在理解模板的时候,时刻和面向对象的设计原则结合起来。

类型安全的知识请自行阅读相关资料。

请多动手写代码,多看看优秀的代码,要理解为什么这样,而不是那样的问题的最好方式就是在实际的工作中去运用,然后维护一段时间,你就能对优秀代码有深刻的体会了。

Launcher | 园豆:45050 (高人七级) | 2012-11-28 13:15

@Launcher: 为什么是:

CDoubleBufferImpl<MyCDoubleBufferImpl>* myImpl = new MyCDoubleBufferImpl;

myImpl->OnPaint(...);

而不是:

MyCDoubleBufferImpl* myImpl = new MyCDoubleBufferImpl;
myImpl->OnPaint(...);

只能说这样疑问很初级,说明你还没学会怎么使用模板,实际上我们是这样来使用的:

template<class T>

class MyContorlImpl : CDoubleBufferImpl<MyContorlImpl>

{

    void DoPaint(CDCHandle dc){// MyControl 的绘制代码}

};

class MyControl : MyContorlImpl<MyControl>

{
};

MyControl ctl;

ctl.OnPaint();

事实上 MyControlImpl 还会继承多个模板类,也不会有OnPaint的显示调用(通过MESSAGE_MAP映射),我这里做了简化,具体的代码请参阅WTL源码。

如果你的需求足够简单,上面的代码会显得冗余,或者称为过度设计,但从整个WTL的体系结构来看,这样的设计提供了很好的可扩展性,可维护性(估计你没维护过代码,所以对这点也没有体会,通常来说就是设计规范,一致的代码结构能够减轻维护者对代码的理解负担)。

所以请你先理解了面向对象的设计原则后,再研究模板。

Launcher | 园豆:45050 (高人七级) | 2012-11-28 13:40

@Launcher: 非常感谢你!你的解释和用例都很详细,虽然我还有疑惑,但从中我多少能够感觉到运用模板的意图。我了解面向对象程序设计,但之前不用模板,或者只是很初级的用法。我也知道第一种方式的能力是非常有限的,但也不知道CRTP的能力究竟在哪,所以会拿来比。正如你说,关键是我不知道实际当中这些模式是怎么被用的,所以很疑惑地提出比较奇怪的问题。今后我会去多读与模板相关的源码,有问题还得向你请教!多谢!

didigene | 园豆:157 (初学一级) | 2012-11-28 14:29

@didigene: 回答你“不知道CRTP的能力究竟在哪”的最好方式,仍然是实践,请用你自己的方式实现一次WTL的如下类:CWindowImpl,CWindow,CDoubleBuffer,要求,别人使用你的基类能够用来实现自己的控件,比如CStatic,CEdit,CButton等基本控件。

经实践证明,在UI库的设计中,只有两种技术可用,一是虚函数(MFC),另一种就是模板(ATL/WTL),前者比后者语义简单,后者比前者性能更好,同时具备静态类型检查功能,那么在使用模板的时候,CRTP完美的解决了在父类中调用派生类方法的问题(因此,你不要拿CRTP来和非模板技术比较,而是要理解CRTP是运用模板技术的一种固定模式,我们讨论模板中的CRTP,等同于讨论设计模式中的抽象工厂(或适配器、组合、媒介等))。

Launcher | 园豆:45050 (高人七级) | 2012-11-28 14:55
其他回答(1)
0

说下我的看法

就楼主说的那种情况,即利用多态批量处理父类的指针或者引用,这个模式是完全做不到的,这不是他的应用点。

我对这个模式的应用点的理解是这样的,举例说明:

一个类库提供了一个类CurveDraw,有一个方法draw(vector<double>, vector<double>);

这个方法接收一组x坐标值,一组y坐标值,然后在屏幕上画出一条线。

如果想让这个类具有可扩展能力,最简单的方法就是将draw定义为virtual。但是这时候会有动态绑定的消耗。

如果照上面的模式写:

templae<typename T = normal>
class CurveDraw{
public:
  void Draw(vector<double> x, vector<double> y) {
      static_cast<T*>(this)->MyDraw(x,y);
  }
};

这样,如果想用库提供的方法,就用CurveDraw<>,如果想自己扩展,就定义类实现MyDraw方法

而且,这样的扩展没有动态绑定的消耗,在实际应用中也基本上不会有多层的继承出现。

收获园豆:5
花无形 | 园豆:279 (菜鸟二级) | 2012-11-23 23:17

非常感谢你的回答!就你说的情况,是否也能用第一种方式实现呢?不用虚函数也不用模板,而是子类直接覆盖父类的函数,因为CRTP使用父类的指针或引用似乎用处不大,这样也不会有虚函数的性能损失。比如自己定义一个MyDraw 继承CurveDraw,然后重写自己感兴趣的函数(假设CurveDraw中没有虚函数)。如果这样可以做到,为什么还需要CRTP,CRTP实现显然要麻烦。

支持(0) 反对(0) didigene | 园豆:157 (初学一级) | 2012-11-27 13:32
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册