前言
在C++简单依赖注入一文中,利用C++类的static变量初始化特性实现了简单的依赖注入框架。你是否还有印象,其中还同时利用C++模板编程另一个特性CRTP:把派生类作为基类的模板参数。
template<typename T>
class ManagedObject {};
class A : public ManagedObject<A> {};
CRTP又是什么鬼?CRTP (Curiously Recurring Template Pattern),翻译中文是奇特递归模板模式,它到底奇特在哪里?用法看起来有点奇怪,怎么能把一个对于基类未知的类型传给基类呢?在C++模板编程中,这一切皆有可能。
多态
我们再回顾一下面向对象编程(OOP)三个重要特性:封装,继承,多态。多态(Polymorphisn)是指一个接口,有多种实现,可以让父类的指针有“多种形态”。C++由于有模板编程,在编译期可以做很多的事情,于是C++中的多态性又体现在编译和运行两个阶段:
- 静态多态:在编译时可以确定使用的接口,静态绑定
- 动态多态:具体引用的接口在运行时才能确定,动态绑定
静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,函数地址的绑定是在编译时期还是运行时期。静态多态在编译期间就可以确定函数的调用地址,并生产代码。动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定。
本文的标题函数(类的成员函数通常也叫类的方法)静态与动态分发,即指类方法的地址绑定是静态(编译期)还是动态(运行期)。
动态分发
我们还是先来一个简单多态例子:
class Animal
{
public:
virtual void Eat() = 0;
};
class Dog: public Animal
{
public:
void Eat() override { cout << "Dog::Eat()" << endl; }
};
class Cat: public Animal
{
public:
void Eat() override { cout << "Cat::Eat()" << endl; }
};
void DynamicDispatch(Animal& animal) { animal.Eat(); }
动态多态通过“继承+虚函数”来实现的,只有在程序运行期间(非编译期)才能判断所引用对象的实际类型,根据其实际类型调用相应的方法。具体格式就是使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,并且派生类需要重新实现该成员函数,编译器将实现动态绑定。在运行期根据虚表决定调用合适的函数,即动态分发。
虚函数(Virtual Function)机制由下面两部分来实现:
- 一张虚函数表(Virtual Table),简称为vtbl或vtable。表中主要保存了一个类中的虚函数的地址,这张表解决了继承、覆盖的问题,保证其内容能真实反应实际的函数,是由编译器自动为一个带有虚函数的类生成一块内存空间,存储每个虚函数的入口地址
- 一个指向虚函数表的指针(vptr)。每一个包含虚函数的类的实例都包含一个vptr指针,在对象实例初始化完成之后,指向虚函数表的首地址
通过vptr指针找到要访问的虚函数,完成虚函数的调用:
- 找到虚函数表的首地址
- 找到要使用虚函数地址,调用虚函数
虚函数能很好地实现了多态的要求,但是在运行时引入了一些开销:
- 对每一个虚函数的调用都需要额外的指针寻址
- 每个对象都需要有一个额外的指针指向虚表
每次的函数调用都要查询虚函数表,因此效率低下。实际上为了提高效率,C++的编译器是保证虚函数表的指针存在于对象实例中最前面的位置,这是为了保证取到虚函数表有最高的性能。
静态分发
还是先上一段使用CRTP的代码:
template<typename Derived>
class Animal
{
public:
void Eat() { static_cast<Derived*>(this)->Eat(); }
};
class Dog: public Animal<Dog>
{
public:
void Eat() { cout << "Dog::Eat()" << endl; }
};
class Cat: public Animal<Cat>
{
public:
void Eat() { cout << "Cat::Eat()" << endl; }
};
template <typename T>
void StaticDispatch(Animal<T>& animal) { animal.Eat(); }
从上面的代码可以看出,CRTP有如下特点:
- 基类是模板类,派生类继承自模板类
- 派生类将自身作为参数传给模板类的特例化
- 基类模板中函数的实现通过static_cast将this指针转换为模板参数(即派生类)类型,并调用派生类的函数版本
从基类对象的角度来看,派生类对象其实就是本身,这样的话只需要用一个static_cast
就可以把基类转化成派生类,从而实现基类对象对派生对象的访问。看到static_cast
或者你会联想到:
- static_cast:静态良性转换,在编译期检查
- dynamic_cast:通常是动态向下(downcast)转换,基类指针(引用)转换派生类指针(引用),运行时检查,需要借助RTTI (Run Time Type Identification) , 是通过对象指针得到vptr,再从vptr得到vtbl,这样能够检查类型信息是否匹配
CRTP的存在是可以消除虚表,避免动态查找虚函数的开销,理论上用到多态的地方都可以用CRTP来改写。
CRTP
CRTP从它的英文全名(Curiously Recurring Template Pattern)来看,它是一种模板设计模式,可以用于静态多态,但不等于静态多态。CRTP另外的用处就是定义抽象方法,作为快速扩展类的手段。基类是模板类,所以在编译时可以随意调用T.fun()而不需要声明,派生类只需要给出fun()的实现即可实现功能。
C++中并没有像Java的Interface的概念,只有纯虑函数基类,我们通常可以把它当成Interface,把CRTP和Interface做个对比:
- CRTP是通过模板参数获得派生类的类型,并为每个派生类都生成了一个基类,每个派生类继承CRTP类都要将自身的类型传给基类,就发生了模板实例化,本质上并没有一个基类(接口)对多个派生类(实现)
- Interface本身不知道派生类的类型,但是它有确定的方法签名,派生类共享一个Interface基类对象,并且每个方法都是通过查找虚表调用
应用示例
enable_shared_from_this
C++11引入std::enable_shared_from_this。当我们有类需要被智能指针share_ptr管理,且需要通过类的成员函数里需要把当前类对象包装为智能指针传递出一个指向自身的share_ptr时。在这种情况下类就需要通过继承enable_shared_from_this,通过父类的成员函数shared_from_this来获取指向该类的智能指针。
using namespace std;
struct Good: enable_shared_from_this<Good> // 注意:继承
{
// 正确写法:继承enable_shared_from_this,使用shared_from_this返回shared_ptr
shared_ptr<Good> getptr() {
return shared_from_this();
}
};
struct Bad
{
// 错误写法:不能shared_ptr(this),它会Double Free
shared_ptr<Bad> getptr() {
return shared_ptr<Bad>(this);
}
};
enable_shared_from_this的实现逻辑并不是本文的重点,它详细的实现建议打开STL的源码深入了解,它主要完成两点:
// 1. 通过自身维护了一个weak_ptr让所有从该对象派生的shared_ptr都通过了weak_ptr构造派生
private:
mutable weak_ptr<_Tp> _M_weak_this;
// 2. shared_ptr的构造函数判断出对象是enable_shared_from_this的之类之后,也会同样通过对象本身的weak_ptr构造派生,这样引用计数是互通的,也就不会存在上述Double Free问题
shared_ptr<_Tp>
shared_from_this()
{ return shared_ptr<_Tp>(this->_M_weak_this); }
单例模板
使用CRTP实现的单例模板:
template<typename T>
class Singleton
{
public:
static T& Instance()
{
static T instance;
return instance;
}
proctected:
Singleton() = default;
virtual ~Singleton() = default;
Singleton(const Singleton &) = delete;
Singleton& operator=(const Singleton &) = delete;
};
class A: public Singleton<A> {}
结语
本文简单介绍C++多态两种实现,基于虚表实现的动态多态,与基于CRTP实现的静态多态。CRTP由于不存在动态查找函数指针,在编译期生成代码,相对虚表实现具有较好的性能。但是由于其怪异的语法,对初学者并不友好,通过CRTP可以看到C++模板的强大魅力,为C++的宏大复杂所倾倒。