奇异递归模板模式
奇异递归模板模式(curiously recurring template pattern,CRTP)是C++模板编程时的一种惯用法(idiom):把派生类作为基类的模板参数。[1]更一般地被称作F-bound polymorphism,是一类F 界量化。
一般形式
// The Curiously Recurring Template Pattern (CRTP)
template<class T>
class Base
{
// methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
// ...
};
静态多态
C++语言的多态,原本是用虚函数来实现的,属于动态多态。安德烈·亞歷山德雷斯庫在Modern C++ Design[5]中提出了奇异递归模板模式,并称之为静态多态(static polymorphism)。
template <class T>
struct Base
{
void interface()
{
// ...
static_cast<T*>(this)->implementation();
// ...
}
static void static_func()
{
// ...
T::static_sub_func();
// ...
}
};
struct Derived : Base<Derived>
{
void implementation();
static void static_sub_func();
};
基类模板利用了其成员函数体(即成员函数的实现)在声明之后很久都不会被实例化(实际上只有被调用的模板类的成员函数才会被实例化),并利用了派生类的成员函数(通过类型转化)。
在上例中,Base<Derived>::interface(),虽然是在struct Derived之前就被声明了,但未被编译器实例化直至它被实际调用,这发生于Derived声明之后,此时Derived::implementation()的声明是已知的。
这种技术获得了类似于虚函数的效果,并避免了动态多态的代价。也有人把CRTP称为“模拟的动态绑定”。[6]
这种模式广泛用于Windows ATL与WTL库,以及Boost.Iterator,Boost.Python或者Boost.Serialization等库中。
考虑一个基类,没有虚函数,则它的成员函数能够调用的其它成员函数,只能是属于该基类自身。当从这个基类派生其它类时,派生类继承了所有未被覆盖(overridden)的基类的数据成员与成员函数。如果派生类调用了一个被继承的基类的函数,而该函数又调用了其它成员函数,这些成员函数不可能是派生类中的派生或者覆盖的成员函数。也就是说,基类中是看不到派生类的。但是,基类如果使用了CRTP,则在编译时派生类的覆盖的函数可被选中调用。这效果相当于编译时模拟了虚函数调用但避免了虚函数的尺寸与调用开销(VTBL结构与方法查找、多继承机制)等代价。但CRTP的缺点是不能在运行时做出动态绑定。
不通过虚函数机制,基类访问派生类的私有或保护成员,需要把基类声明为派生类的友元(friend)。如果一个类有多个基类都出现这种需求,声明多个基类都是友元会很麻烦。一种解决技巧是在派生类之上再派生一个accessor类,显然accessor类有权访问派生类的保护函数;如果基类有权访问accessor类,就可以间接调用派生类的保护成员了。这种方法被boost的多个库使用,如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access。原理示例代码如下:
template<class DerivedT> class Base
{
private:
struct accessor : DerivedT
{ // accessor类没有数据成员,只有一些静态成员函数
static int foo(DerivedT& derived)
{
int (DerivedT::*fn)() = &DeriveT::do_foo; //获取DerivedT::do_foo的成员函数指针
return (derived.*fn)(); // 通过成员函数指针的函数调用
}
}; // accessor类仅是Base类的成员类型,而没有实例化为Base类的数据成员。
public:
DerivedT& derived() // 该成员函数返回派生类的实例的引用
{
return static_cast<DerivedT&>(*this);
}
int foo()
{ // 该函数具体实现了业务功能
return accessor::foo( this->derived());
}
};
struct Derived : Base<Derived> // 派生类不需要任何特别的友元声明
protected:
int do_foo()
{
// ... 具体实现
return 1;
}
};
例子1:对象计数
统计一个类的实例对象创建与析构的数据。[7]可以轻松地利用CRTP实现:
template <typename T>
struct counter
{
static int objects_created;
static int objects_alive;
counter()
{
++objects_created;
++objects_alive;
}
counter(const counter&)
{
++objects_created;
++objects_alive;
}
protected:
~counter() // objects should never be removed through pointers of this type
{
--objects_alive;
}
};
template <typename T> int counter<T>::objects_created( 0 );
template <typename T> int counter<T>::objects_alive( 0 );
class X : counter<X>
{
// ...
};
class Y : counter<Y>
{
// ...
};
例子2:多态复制构造
当使用多态时,常需要基于基类指针创建对象的一份拷贝。常见办法是增加clone虚函数在每一个派生类中。使用CRTP,可以避免在派生类中增加这样的虚函数。
// Base class has a pure virtual function for cloning
class Shape {
public:
virtual ~Shape() {}
virtual Shape *clone() const = 0;
};
// This CRTP class implements clone() for Derived
template <typename Derived>
class Shape_CRTP : public Shape {
public:
virtual Shape *clone() const {
return new Derived(static_cast<Derived const&>(*this));
}
};
// Nice macro which ensures correct CRTP usage
#define Derive_Shape_CRTP(Type) class Type: public Shape_CRTP<Type>
// Every derived class inherits from Shape_CRTP instead of Shape
Derive_Shape_CRTP(Square) {};
Derive_Shape_CRTP(Circle) {};
This allows obtaining copies of squares, circles or any other shapes by shapePtr->clone()
.
例子3:不可派生的类
一个类如果不希望被继承,类似于Java中的具有finally性质的类,这在C++中可以用虚继承来实现:
template<typename T> class MakeFinally{
private:
MakeFinally(){}//只有MakeFinally的友类才可以构造MakeFinally
~MakeFinally(){}
friend T;
};
class MyClass:public virtual MakeFinally<MyClass>{};//MyClass是不可派生类
//由于虚继承,所以D要直接负责构造MakeFinally类,从而导致编译报错,所以D作为派生类是不合法的。
class D: public MyClass{};
//另外,如果D类没有实例化对象,即没有被使用,实际上D类是被编译器忽略掉而不报错
int main()
{
MyClass var1;
// D var2; //这一行编译将导致错误,因为D类的默认构造函数不合法
}
例子4:std::enable_shared_from_this
在C++标准库头文件<memory>
中,std::shared_ptr
类封装了可被共享使用的指针或资源。一个被共享的对象不能直接把自身的原始指针(raw pointer)this
传递给std::shared_ptr
的容器对象(如一个std::vector),因为这会生成该被共享的对象的额外的共享指针控制块。为此,std::shared_ptr
API提供了一种类模板设施std::enable_shared_from_this
,包含了成员函数shared_from_this
,从而允许从this创建一个std::shared_ptr
对象。
class mySharedClass:public std::enable_shared_from_this<mySharedClass>{
public:
// ...
};
int main()
{
std::vector<std::shared_ptr<mySharedClass>> spv;
spv.push_back(new mySharedClass());
std::shared_ptr<mySharedClass> p(new mySharedClass());
mySharedClass &c=*p;
spv.emplace_back(c.shared_from_this());
}
其它语言
在Java与.NET Framework中,常见把一个类作为泛型超类或interface的类型参数。例如,下述Java类实现了标准库中的泛型界面Comparable
:
public class Item implements Comparable<Item> {
private String name;
@Override
public int compareTo(final Item other) {
return name.compareTo(other.name);
}
}
这保证了编译时不会比较Item
与其它不是Item
的对象如String
。与C++不同,Java类不能使用不同的类型参数扩展一个超类两次,也不能用不同的类型参数实现同一个interface两次。这是由于Java实用了类型擦除处理泛型。例如,上例的class Item
不能同时实现Comparable<Item>
与Comparable<Object>
。详见:en:Generics in Java § Problems with type erasure。
参见
- Barton–Nackman trick
- F-受限量化
参考文献
- Abrahams, David; Gurtovoy, Aleksey. . Addison-Wesley. 2005. ISBN 0-321-22725-5.
- William Cook; et al. (PDF). 1989 [2015-07-14]. (原始内容存档 (PDF)于2015-02-10).
- Coplien, James O. (PDF). C++ Report. February 1995: 24–27.
- Budd, Timothy. . Addison-Wesley. 1994. ISBN 0-201-82080-3.
- Alexandrescu, Andrei. . Addison-Wesley. 2001. ISBN 0-201-70431-5.
- . 7 May 2003 [13 January 2012]. (原始内容存档于2012年2月9日).
- Meyers, Scott. . C++ User's Journal. April 1998 [2015-07-14]. (原始内容存档于2015-07-16).