奇異重現模板模式
奇異重現模板模式(curiously recurring template pattern,CRTP)是C++模板編程時的一種慣用法(idiom):把派生類作為基類的模板參數。[1]更一般地被稱作F-bound polymorphism,是一類F 界量化。
歷史
[編輯]1980年代作為F 界量化被提出。[2]Jim Coplien於1995年稱之為CRTP。[3]在當時的早期C++模板超編程已經有所出現。[4]
一般形式
[編輯]// 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。
參見
[編輯]參考文獻
[編輯]- ^ Abrahams, David; Gurtovoy, Aleksey. C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond. Addison-Wesley. 2005. ISBN 0-321-22725-5.
- ^ William Cook; et al. F-Bounded Polymorphism for Object-Oriented Programming (PDF). 1989 [2015-07-14]. (原始內容存檔 (PDF)於2015-02-10).
- ^ Coplien, James O. Curiously Recurring Template Patterns (PDF). C++ Report. February 1995: 24–27.
- ^ Budd, Timothy. Multiparadigm programming in Leda. Addison-Wesley. 1994. ISBN 0-201-82080-3.
- ^ Alexandrescu, Andrei. Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley. 2001. ISBN 0-201-70431-5.
- ^ Simulated Dynamic Binding. 7 May 2003 [13 January 2012]. (原始內容存檔於2012年2月9日).
- ^ Meyers, Scott. Counting Objects in C++. C++ User's Journal. April 1998 [2015-07-14]. (原始內容存檔於2015-07-16).