奇異重現模板模式
奇異重現模板模式(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).