学会了这么神奇的模版模式,让你C++模版编程之路事半功倍

最近由于开发工作的需要,项目引入了boost::statechart的状态机,它大量了引用了CRTP,  它的全称是Curiously Recurring Template Pattern,奇异递归模版模式,C++模版编程中很常用的一种用法。那么它神奇的地方到底在哪里呢,接下来就一一来揭开它神秘的面纱。

一、奇异递归模版模式的简介

奇异递归模版模式的基本思想要点是:派生类要作为基类的模版参数。它是C++模版编程中常用的手法。理解它之后,学习模版编程过程中也会事半功倍,而不会觉得云里雾里的。

二、奇异递归模版模式的基本格式

奇异递归模版模式的基本格式如下:JCrtpDerived继承JCrtpBase,并且JCrtpDerived作为基类JCrtpBase的模版参数。通过这样的方式,基类就可以使用子类的方法。并且不需要使用到虚函数,一定程度上减少程序的开销。

template <typename T>
class JCrtpBase
{
public:
};

class JCrtpDerived : public JCrtpBase<JCrtpDerived>
{
public:

};

三、奇异递归模版模式的入门

从上面的给出的奇异递归模版模式的基本格式中可以看出,子类是作为基类的模版参数,但是如果传递给基类的模版参数不是基类的子类,那就会造成混乱错误。如下图所示,JCrtpDerived2子类继承了基类JCrtpBase,但是传递给基类的模版参数不是JCrtpDerived2。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:

};

class JCrtpDerived2 : public JCrtpBase<JCrtpDerived1>
{
public:

};

那么如何解决上面的问题呢,可以将基类的默认构造函数设置为私有,并且模版参数T设置为基类的友元。通过这样的方式,基类的构造函数只能由模版参数T调用。当创建JCrtpDerived2子类对象的时候,会调用基类的构造函数,而这时候发现JCrtpDerived2不是基类的友元,那么就无法调用基类构造函数而出错。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

private:
   JCrtpBase();
   friend T;
};

调用运行JCrtpDerived2,就会出现错误

JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

四、奇异递归模版模式的应用场景

1、静态多态

奇异递归模版模式可以实现静态多态的效果,顾名思义,就是有多态的特性,但是不需要使用虚函数,是编译的时候确定,因此,能够减少运行时的开销。接下来就来看看两个示例。

基类JCrtpBase实现函数Do,该函数内部对象通过static_cast转换为模版参数对象,模版参数对象再调用对应的实现函数,而模版参数对象由子类来实现。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
        derived->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

调用运行的效果如下所示,从中可以看出,对象调用基类的函数,而基类函数实际上又去调用子类的函数DoSomething。基于这样的思想,我们可以将通用的逻辑放在基类Do中实现,而不同的放到对应的子类函数DoSomething实现。

/// 调用   
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2

这样需要注意的一点是,如果子类再被其他子类继承,那么其他子类就不能按照上面的方式实现。具体可以看下示例:JCrtpSub子类再继承JCrtpDerived1。

class JCrtpSub: public JCrtpDerived1
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpSub";
    }
};

调用运行的效果如下所示,JCrtpSub调用基类的函数Do,但是运行没有调用到JCrtpSub类自身的函数DoSomething。

/// 调用
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

JCrtpSub ctrp_sub;
ctrp_sub.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1

上面的例子是子类调用基类函数,由基类再转换调用子类函数,效果类似于策略模式。下面将要说明的例子,更像多态特性,但是不需要虚函数。基类和子类都实现相同的函数DoSomething

template <typename T>
class JCrtpBase
{
public:
    void DoSomething()
    {
        static_cast<T *>(this)->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

然后实现模版方法,该方法入参为基类JCrtpBase的引用,内部调用基类函数DoSomething。

template<typename T>
void DoAction(JCrtpBase<T> &ctrpbase)
{
    ctrpbase.DoSomething();
}

调用运行效果如下,向模版方法传递不同的子类,调用对应子类的函数。

/// 调用
JCrtpDerived1 crtp_derived1;
JCrtpDerived2 crtp_derived2;
DoAction(crtp_derived1);
DoAction(crtp_derived2);

// 打印信息
[void JCrtpDerived1::DoSomething():38] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():48] I'am is JCrtpDerived2

2、boost::statechart状态机

Boost.Statechart大量使用了CRTP,   派生类必须作为第一个参数传递给所有基类模版,Boost.Statechart状态机后续考虑作为一个专题来研究讨论。

struct Greeting : sc::simple_state< Greeting, Machine >

3、std::enable_shared_from_this特性

C++的特性enable_shared_from_this通常是用于在当你要返回一个被shared_ptr管理的对象。JObj继承enable_shared_from_this,并且JObj作为参数模版传递给enable_shared_from_this,这里就运用到了CRTP。

class JObj : public std::enable_shared_from_this<JObj>
{
public:
    std::shared_ptr<JObj> GetObj() {
        return shared_from_this();
    }
};

正确的调用方式,JObj是被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this。

std::shared_ptr<JObj> share_obj1 = std::make_shared<JObj>();
// JObj对象被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this
std::shared_ptr<JObj> share_obj2 = share_obj1->GetObj();

五、总结

到这里,奇异递归模版模式已经基本讲解完成,我们首先介绍了它的基本格式,使用注意要点,然后重点讲解了它的应用场景,包括静态多态、boost::statechart状态机、std::enable_shared_from_this特性。理解了奇异递归模版模式,不但有利于模版编程的学习,而且对于以后应用的开发也是有好处的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注