C++之CRTP手法

初次相识CRTP就是boost::enable_shared_from_this,这种以派生类型作为模板参数生成基类的形式,让人看着很新奇。

参阅了很多的文献,大多是讲使用CRTP可以实现编译器静态多态的效果,并与之同传统的虚函数实现多态进行对比,这里也类似的探究一下多态的特性。

一、C++的多态实现

1.1 传统的虚函数实现多态的原理

我们通常讨论的C++之多态是通过虚函数的方式来实现的,其被称为运行时刻的多态(dynamic polymorphism, runtime polymorphism),因为程序运行时刻具体调用的函数实体是根据指针或者引用实际指向的实际对象类型来决定的,这就是C++的延迟绑定或者称为动态绑定的特性。

在C++中,上述特性是通过vtable和vptr来实现的,比如某个基类Base具有function1和function2两个虚函数,并以其为基类得到两个派生类D1和D2,两个派生类D1和D2分别override了基类的function1和function2虚函数,这种关系下的vtable则形如下图所示:

在C++中,每一个使用了虚函数的类(或者从含有虚函数的类派生出来的子类)都会拥有一个vtable,该vtable是在编译期间由编辑器产生的一个静态数组,该数组中的每个条目则指向了该类型可以访问的最接近的虚函数。这里最近可访问的函数就意味着,如果派生类override了基类的虚函数,则该vtable中的对应条目就指向派生类的虚函数,如果派生类没有override基类的虚函数,则该条目就直接指向其基类的虚函数版本。

这里的vtable是与类类型相关的,而不针对于任何创建出来的具体对象,所有该类型的对象都共用该vtable,但是程序的执行依赖于创建的对象来运行的,所以编译器在创建vtable的同时还创建了vptr成员变量,该变量还会被所有的派生类所继承,当使用一个具有vtable的类型创建一个对象的时候,该vptr成员就指向了其具体对象类型的vtable。

通过上面的这些操作,C++通过虚函数实现运行时绑定就显而易见了:程序在执行一个函数调用的时候,如果发现该函数是一个虚函数,则用其vptr成员访问其动态类型关联的vtable,然后在vtable中找到合适的虚函数版本进行实际的函数调用。不过经过上面的步骤,我们会发现使用虚函数机制,会让每个对象增加一个额外的vptr大小的开销(比如64位系统上就是8字节);同时在调用虚函数的时候还需要查找虚函数表去寻求正确的函数版本,所以会有人诟病虚函数机制会对函数调用性能产生负面影响;还有就是虚函数通常不能被inline,对于一些尺寸小的高频调用函数将无法得到内联的优化。

1.2 CRTP实现静态多态

和上面运行时多态相对应,C++还可以实现静态多态(static polymorphism)手法,以消除C++虚函数实现的动态多态带来的负面影响,这是通过C++模板技术来实现。

传统方式说来,基类只能访问基类本身的成员,所以不借助vtable特性的话基类无法访问派生类的任何成员信息。但是C++模板类有一个特性,就是只需要在其实例化的时候也就是实际使用该类型创建对象的时候)能得到其定义就可以了,所以得以可以将派生类的真实类型事先注入到基类当中,在基类的实现中就可以安全的进行下行转换,并调用派生类的成员信息,而且这种手法被编译器认为是合法的。在C++中,这种Idioms被称为CRTP(Curiously Recurring Template Pattern),在boost库中该手法常常被使用。

其实,有过模板编程经验的人都知道,C++模板作为补充,常常被大量的用来改善和优化传统C++编程的各种问题。

template <class T>
struct Base { 
    void interface(){ 
        static_cast<T*>(this)->implementation(); 
        // static_cast<T&>(*this).implementation();
    }
    static void static_func(){ 
        T::static_sub_func(); 
    }
};

struct Derived: Base<Derived> { 
    void implementation(){
        std::cout << "impl called" << std::endl;
    }
    static void static_sub_func(){
        std::cout << "static sub called" << std::endl;
    }
};

Derived d; d.interface(); d.static_func();
Base<Derived>* p = new Derived; p->interface(); p->static_func();

CRTP的基本使用形式上面所示。首先一眼看到这个不要觉得过于的沮丧,因为毕竟没有使用额外的vtable和vptr,所以提供的多态功能有限,但是从形式上看来我们可以发现,这个Idiom和之前的 《C++之虚函数的访问性》 中提到的NVII(Non-Virtual Interface Idiom)看上去是何其的相似。上面的代码等同于在基类中事先知晓了(或者说约定了)模板参数必须实现的接口implementation和静态接口static_sub_func,否则实例化的时候使用代码将无法通过编译,同时正如NVII的优点一样,实现部分和调用部分的接口可以不一致,而且在接口中也可以额外的添加一些通用的前置、后置代码。

注意上面的interface和implementation必须是不同的名字,因为C++的名字查找特性导致派生类会隐藏基类中的同名函数。

二、CRTP的使用场景

正如上面的例子所示,任何使用NVII的地方,都可以考虑使用CRTP的手法来得到运行时性能的优化,虽然现代处理器这点优化带来的收益可能不那么的明显,但至少给我们提供了一个解决问题新的思路。不过如果CRTP只能用来优化这么虚函数调用这一点点边界性能的话,那么其作为一个Idiom要被推广的话肯定没有什么说服力吧。

其实,CRTP最常见的使用,除了像上面所描述的用来定义接口并实现静态多态功能,还有就是设计一些和特定用户类相关的helper类,用户类在使用的时候以自己作为模板参数,创建实例化的helper类为自己的基类,从而获得某些特定的功能或接口(Adding Functionality),这样的好处是:helper类可以知道子类的类型信息,并且可以调用子类相关约定的接口;其次派生类可以继承多个helper类以让自己的功能得到丰富和扩充,虽然使用非成员的模板函数也能实现上述功能,但是CRTP会让代码组织管理的更加清晰可维护。虽然在C++当中多继承并不被推荐而极力强调组合模式,但是实际上此处的继承和C++传统继承在某些意义上是有所区别的,CRTP中的继承实际是派生类为基类提供了接口,基类利用派生类的接口实现或者增加一些功能、特性,有区别于传统的”is-a”的关系。

2.1 对象计数

对象计数可以说是最常被拿来说明CRTP的例子了,也有可能是Meyers的 影响力 太广泛的原因吧。因为CRTP中给基类传递不同的模板参数就会被实例化成不同的类型,所以在这个实例化的基类的构造、析构函数中进行对象计数是再合适不过的了,而且根据拷贝、移动等特性还可以玩一些其他的小跟踪和小统计,此处就不表了。

2.2 单例类

如果要借助CRTP的方式实现一个单例类helper,那么派生类的参数就必须要传递给helper,以便在合适的时候构造该类型的实际对象。不过网上的留言说是当今Singleton模式已经不再推荐使用了,有时间还需要了解一下为啥,毕竟自己的项目中使用了很多的单例类。

template <typename T> 
class Singleton {
public:
    static T& GetInstance(){
        static T helper;
        return helper;
    }
protected:
    Singleton(){}
    virtual ~Singleton(){}
private:
    Singleton(Singleton const &);
    Singleton& operator= (Singleton const &); 
};
struct Test: public Singleton<Test> {
    // ...
};

2.3 重载比较运算符

这也是一个被举的烂烂大街的例子了,其实本质上说,通过在基类定义virtual operator==,也可以得到相同的效果。

template<typename T>
class Comparable{
};
template <typename T>
bool operator == (Comparable<T> const& lhs, Comparable<T> const & rhs) {
    T const& d1 = static_cast<T const&>(lhs);   
    T const& d2 = static_cast<T const&>(rhs); 
    return !(d1 < d2) && !(d2 < d1);
}
template <typename T>
bool operator != (Comparable<T> const& lhs, Comparable<T> const & rhs) {
    return !(lhs == rhs);
}
struct Apple: public Comparable<Apple> {
    int size_;
};
bool operator< (Apple const& lhs, Apple const& rhs) {
    return lhs.size_ < rhs.size_;
}

2.4 扩展派生类的功能

这里说是扩展派生类的功能,也可以说是重构过程中抽象出某些公共的功能。当然这里每次写static_cast会有些累赘,可以将这个转换提取成一个内联成员函数就方便些了。

template <typename T>
struct NumericalFunctions {
    void scale(double multiplicator){
        T& underlying = static_cast<T&>(*this);
        underlying.setValue(underlying.getValue() * multiplicator);
    }
    void square(){
        T& underlying = static_cast<T&>(*this);
        underlying.setValue(underlying.getValue() * underlying.getValue());
    }
};

struct Sensitivity: public NumericalFunctions<Sensitivity> {
    double getValue()const { return value_; }
    void setValue(double value){ value_ = value; }
    double value_;
};

虽然上面的例子可以通过将scale和square两个操作使用单独的非成员模板函数来实现,但是显然用这种CRTP的手法,代码组织的更为清晰,其他派生类如果需要实现计算功能也只需要以自身类型为模板参数派生自NumericalFunctions就可以了。

2.5 enable_shared_from_this

enable_shared_from_this看似很神奇,起来也挺简单。查看其 代码 真的发现也没有什么东西,无非就是在weak this 中保存了this智能指针的某个若引用,在需要使用this指针的时候对弱引用进行提升并返回。

shared_ptr<T const> shared_from_this() const {
     shared_ptr<T const> p( weak_this_ );
     return p;
}

使用shared_from_this()比较多的情况,常常出现在boost::bind封装成员函数的位置,以及boost::asio这类异步库经常需要触发成员函数回调的位置。反正现代C++的法则是尽量避免new、delete操作原始指针,同时也尽量减少this裸指针的使用哦。

其实看完上面的例子,觉得CRTP的使用场景也就是:基类为派生类提供某种功能或者特性,但是需要知道派生类的类型信息,或者约定访问派生类的某些特定接口。原理就是这么简单,关键用溜起来才是王道!

本文完!

我来评几句
登录后评论

已发表评论数()