http://www.ddj.com/cpp/218600111;jsessionid=A3DTTZ4H5WD3EQSNDLRSKH0CJUNN2JVN?pgno=2
範型這東西平時本來就很少會用到,除非寫通用的庫。concept對寫庫來說的確是好東西,但是這也不應該是在描述耦合,而是描述一種特徵,STL對迭代器的特徵判斷是用的重載,但是對std::list的iterator遇到std::find仍然無能爲力。本來範型的設計都是基於一個特徵,就像類繼承的設計是基於一個基類一樣
concept存在的意義應該和類繼承一樣重要,沒有concept寫範型代碼就好比在用C在實現OO一樣,一切機遇自己的約束。有的人也會懷疑這些玩意,我覺得如果質疑模板/Concept的必要性其實就等同於站在C的角度質疑OO。那DDJ的文章也說了,現在移除掉concept是因爲有一些技術問題沒解決.
覺得concept就是模板(靜態多態)世界中的“接口”,跟OO中動態的“接口”對照一下,至少可以加深理解。
但從應用的角度看,它們有一點微妙的差別:C++在C的基礎上引入繼承和多態,不僅僅使得OO代碼更加優雅,而且更加安全,這一點是實實在在的。但concept,我覺得僅僅使得模板代碼更加優雅,談不上更加安全。
說得再具體一點:語言對繼承和多態的支持,使得許多在運行期才能發現的錯誤現在可以在編譯時發現,這個好處是很實在的。concept與此類似:許多在“編譯模板實例代碼時”才能發現的錯誤現在在“實例化模板時”就可以發現。——但問題是:編譯器對模板的這些處理細節對程序員是透明的,所以,這一次引入“接口”所帶來的“進步”也就顯得更加“透明”了,不那麼實在了。
本來模板的實例化動作全在編譯期完成,C++也不存在運行時的模板。但是這不應該判斷是否作爲語言元素的一個準則,可以說concept是在完善範型機制,其次有更友好的編譯錯誤應該算是一個附屬產物,比如,回到那個std::list的問題。現在有啥辦法可以讓std::find支持std::list::iterator,難道只有在兩個std::find的後面分別加random_accescc_iterator* = (iteartor_tag*)(0)和input_iterator* = (iteartor_tag*)(0)的參數來區分兩個迭代器?SFINAE雖然是不錯,但是如果一個語言啥都好各種手法或者trick來處理的話,未免也太過複雜了。本來重載是一個好東西,如果能基於concept的重載那就更好了。只是concept本身太複雜了,只是由庫去實現基本上不可能了,如果引入到語言,那麼STL基本會被重寫,這也不是一個小的工程。如果concept有更簡單的實現,估計那個委員會肯定樂意加入。
其實C++0x本來引入的concept是一個很強大的東西,不僅包含模板的類型檢測,還包括,concept-map, concepte-based overload,這些都是讓模板的更容易使用,更強大。
例如本來可能會引入的for-each loop.
1
2
3
4
|
vector< int > v; for ( int x : v) cout<<x<<endl; |
這些都需要concept的支持。同時concept一個存在的目的也是爲消除一些複雜的模板trick。
當然,所有東西在使用上都需要有正確的把握,我覺得這個就是和經驗有關了。OO說,把複雜的東西簡化爲各種小問題,抽象會可複用的模塊,至於劃分到多小,那就是經驗決定的,劃分歸納得不夠會增加耦合,劃分得太細不僅耦合了而且複雜度倒還增加了。
對於concept來說,就算語言引入這樣一個東西,別人可能也不會花上大量的精力去爲自己的類模板寫這些。其實這個問題可以退後一步來說,在平時的開發中,模板真正會用到嗎?我覺得也不會,在平常的開發中,都是寫一些目的明確的代碼。
說concept像“接口”,感覺上是有那麼一回事。一般OO的觀點來說,接口其實就是一組共有的特徵,你提供的東西必須要基於這些特徵。而對於C++的模板來說,concept並不是用來描述一個類模板的,而是用來描述模板參數的,與算法有關。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
concept Swimmable< typename T> { void T::swim(); } template < typename T> requires Swimmable<T> struct Pool { void swim() { T t; t.swim(); } }; |
按照現在模板的特性來說,只有當調用了Pool::swim纔會判斷得出給定的模板參數T是否符合Pool這個模板的要求。如果有了concept,編譯器立馬就知道給定的模板參數T是否符合要求。其次,就算引入了concept並不會帶來什麼麻煩呀,它也並不會對以有的代碼造成任何影響。
或許太多的思考把concept和“接口”捆綁在一起,將“接口”和concept來做比較未免有點太牽強了。首先,C++的範型編程解決的問題是用來寫程序庫上,C++的範型庫有一個最大的優點就是,不像類框架那樣,你必須要從一個“接口/基類”上派生才能在框架中工作,範型庫則不需要你完成這樣的前提,其次,範型庫是特殊類型特殊處理,也就是說,不同的類型你扔進到一個範型庫中,不需要你在乎扔進去的是什麼,這點和OO的多態很像,注意一點的是,如果你用“接口”就必須回到那個前提。如果把concept單純地看成“接口”那必然會帶來束縛,現在OO的束縛就是你需要一個派生,當然這點在OO中不叫束縛,或者叫實現,但從模板的觀點上來說,何嘗不是束縛呢?例如,定義一個vector<類> 和vector<bool>的變量,你不用考慮vector<bool>會浪費掉不用的內存,用distance()的時候,你可以不用考慮你傳入的iterator是哪種category的,但是這還是不夠的,STL的典型問題就是std::find()對iterator有operator<的比較,如果是非random_access_iterator,那麼是沒有operator<的,當然這就暴露出模板的缺陷,如果有了concept,那麼就可以判斷出你給的iterator是否有operator<,如果沒有則選用由operator!=做比較的std::find()。
還有一個典型問題是算法的實現是用SFINAE來判斷迭代器的類型,但是我們在大多數情況下,不會在一個類中包含額外的信息,例如,你要自己實現一個兼容STL的算法iterator就必須包含一個iterator_category的typedef,也就是滿足iterator的traits。如果有了concept,那這traits就完全不用了,算法可以基於concept重載。如果模板即支持(偏)特化,又支持concept,那對寫通用的範型庫帶來極大的便利。
concept倒是有一點比OO中的接口好:就是符合某一concept的實體不必顯式的聲稱這件事情。這一點比OO中的接口省事。
看來我前面對於boost::thread可能要依賴於boost::date_time的擔心是想多了。實際情況是concept不會在依賴方面造成更高的耦合。更正一下。
正確的考量應該是:boost::thread規定它所require的時間概念,而boost::date_time中的類型只是“恰好”符合這一概念。這符合“接口應該由使用接口的client來定義,而非由實現接口的server來定義”的原則。
或許太多的思考把concept和“接口”捆綁在一起,將“接口”和concept來做比較未免有點太牽強了。
兄臺所說的"接口"受了太多OO思想的影響,有點狹隘了.
接口,在一定程度上應該等同於protocol,rules.它僅僅是規範了交互的方式.從這點上來說,concept的本義也如此.
誠然,concept對於消除現在的template triks是很有意義的.但鑑於C++本身的複雜性,要把concept做得很完備,則必須引入更多的特性,到時候,template triks也許就變成了concept tricks...而現有的這些tricks又不能徹底去除.這就是concept現在的兩難境地.而只做一部分concept支持又顯然不是C++的風格.
既然現有的技術可以滿足幾乎所有的要求,就算它很噁心,但大家吐啊吐得也已經習慣得差不多了.所以,我覺得,在沒有把握的情況下,也就沒有必要再引入另一個同樣功能而且還可能是另一個陷阱的東西了.
不過話又說回來.我覺得,如果用concept的形式,再加上type traits那種東西應該滿足concept的設計要求吧.雖然這樣只是換湯不換藥.不過總比BOOST裏那種狗屎類型約束要好看得多吧...
其實type traits也好,繼承也好,他們之間的約定是在你寫一個類的時候發生的,例如,你要在類裏面包含一組traits,或者繼承一個接口/基類。這樣的範型未免太幼稚。
考慮(偏)特化,用於處理特殊的模板參數而做的匹配,並沒有讓使用者在用這個模板之前告訴模板需要選擇哪個特化版本。拿vector做例子
1
2
3
4
5
|
class A{}; vector<A> va; vector< bool > vb; |
模板在選擇特化版本的時候,並不是由A和bool給出的信息去選擇vector的版本,而是由模板自己去匹配該選哪個版本,這是traits方法做不到的。所以特化被看作模板一個很重要的特性。concept同樣如此,當你在寫一個類時,這個類將作爲某個模板的參數時,concept 不會給你任何限制,而對於這個模板來說,它通過用concept的匹配就明確知道了這個模板參數具有哪樣的接口,這樣就可以爲各種模板參數做不同的處理。
在開始,Bjarne也支持用與boost concepts相似的方法,D&E中有這樣的描述,但是那樣只能做到check,遠遠不夠。其實模板上,對接口(成員函數)不同做不同的處理這樣的需求早就有了,而對於這點,就算SFINAE也無能爲力。
例如,考慮一下 事件回調的,例如所有的控件都有一個成員函數叫make_event,回調函數可以是一個函數對象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
template < typename Function> void make_event( int event_type, Function f); //用於註冊回調的接口 -- //一般一個事件回調都會包含一個事件的信息,例如鼠標的座標 class functor_with_eventinfo { public : void operator()( const eventinfo& ei) { if (ei.mouse_x > 10 && ei.mouse_y){} } }; //那麼在,要註冊這個回調,就是 button.make_event(click, functor_with_eventinfo()); //其實大多時候,並不需要事件的信息,也就是說,我們可以寫一個這樣的functor class functor_without_eventinfo { public : void operator()() { messagebox( "hello, world" ); } }; button.make_event(click, functor_without_eventinfo()); |
這種情況的make_event倒容易解決。重載一下就搞定了。
1
2
3
4
5
6
7
8
9
10
11
12
|
template < typename Function> void make_event( int event_type, Function f) { _m_make_event(event_type, f, &Function::operator()); } template < typename Function> void _m_make_event( int event_type, Function f, void (Function::*)()); template < typename Function> void _m_make_event( int event_type, Function f, void (Function::*)( const eventinfo&)); |
現在問題來了,萬一那個make_event的模板,由於歷史原因,並不是所有的模板參數Function的類型都有operator(),有的是fire()
1
2
3
4
5
6
7
8
9
10
11
|
class fire_event_object { public : void fire( const eventinfo&) { messagebox( "hello, world" ); } }; button.make_event(click, fire_event_object()); //注意make_event中是用&Function::operator()判斷的 |
上面的代碼不能工作了。。。其實這種問題已經存在於現實的代碼中。STL的random_access_iterator就有operator+(int)的重載,而forward_iterator卻沒有。因此這種得約定只有靠預先在iterator的定義時包含category的玩意,問題是這樣的動作是否符合範型呢?
OOP的黃昏
在“C++ Template”一書中,將多態總結爲三種主要類型:runtime bound、static unbound和runtimeunbound。其中runtime bound就是我們通常所說的動多態,OOP的核心支柱(廣義上OOP還包括ObjectBase(OB,僅指類型封裝等OO的基本特性),但有時也會將OB和OOP分開,OOP單指以OO爲基礎的動多態。這裏使用狹義的OOP含義);static unbound就是靜多態,通過模板實現。而runtimeunbound則是一種不常見的形式。早年的SmallTalk具有這種形式,現在的ruby也引入這種機制。
在主流的(靜態)語言中,我們會面臨兩種類型的多態需求:對於編譯期可以確定類型的,使用靜多態,比如實例化一個容器;對於運行期方能確定類型的,則使用動多態。而runtimeunbound也可以用於運行期類型決斷。於是,便有了兩種運行期多態。這兩種多態的特性和他們的差異,是本文的核心。實際上,相比動多態,runtime unbound多態爲我們提供了更本質的運行時多態手段,我們可以從中獲得更大的收益。但是鑑於一些技術上的困難,runtimeunbound多態無法進入主流世界。不過,由於新的編程技術的出現,使得這種更好的運行時多態形式可以同動多態一比高下。
動多態
廢話少說,讓我們從一個老掉牙的案例開始吧:編寫一個繪圖程序,圖形包括矩形、橢圓、三角形、多邊形等等。圖形從腳本(比如xml)中讀出,創建後保存在一個容器中備查。通過遍歷容器執行圖形繪製。就這麼個題目,很簡單,也很熟悉,解釋OOP的動多態最常用的案例。下面我們就從動多態實現開始。
首先定義一個抽象基類,也就是接口:
class IShape
{
virtual void load(xml init)=0;
virtual void draw(monitor m)=0;
...
};
然後定義各種圖形類,並從這個接口上繼承:
class Rectangle: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
class Ellipse: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
...
void DrawShapes(monitor m, vector<IShape*> const& g)
{
vector<IShape*>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b)->draw(m);
}
}
...
現在可以使用這些圖形類了:
vector<IShape*> vg;
vg.push_back(new Rectangle);
vg.push_back(new Ellipse);
...
DrawShapes(crt, vg);
通過接口IShape,我們可以把不同的圖形類統一到一種類型下。但是,通過虛函數的override,由圖形類實現IShape上的虛函數。這可以算老生常談了。動多態的核心就是利用override和latebound的組合,使得一個基類可以在類型歸一化的情況下,擁有繼承類的語義。OOP設計模式大量運用這種技術,實現很多需要靈活擴展的系統。
Runtime Unbound
Runtime Unbound多態混合了靜多態和動多態的特徵,即既有類型泛化,又是運行時決斷的。一個最典型的例子就是ruby的函數:class x
def fun(car)
car.aboard
end
end
這個案例非常明確地展示出了Runtime Unbound多態的特點。car參數沒有類型,這裏也不需要關心類型,只要求car對象有一個aboard方法即可。由於ruby是動態語言,能夠運行時檢測對象的特徵,並動態調用對象上的方法。
在Runtime Unbound的思想指導下,我們利用一種僞造的“動態C++”,把上面的繪圖例子重新編寫:
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
void DrawShapes(monitor dev, vector<anything> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<anything> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
...
DrawShapes(crt, vg);
從這段代碼中,我們可以看出RuntimeUnbound多態帶來的好處。所有圖形類不再需要歸一化成一個類型(抽象接口)。每個類只需按照約定,實現load、draw等成員函數即可。也就是說,這些圖形類解耦合了。一旦類型解耦,便賦予我們很大的自由度。最典型的情況就是,我們需要使用一個其他人開發的圖形類,並且無法修改其實現。此時,如果使用動多態,就很麻煩。因爲儘管這些圖形類都擁有load、draw等函數,但畢竟不是繼承自IShape,無法直接插入容器。必須編寫一個繼承自IShape的適配器,作爲外來圖形類的包裝,轉發對其的訪問。表面上,我們只是減少一個接口的定義,但RuntimeUnbound多態帶來的解耦有着非凡的意義。因爲類耦合始終是OOP設計中的一個令人頭痛的問題。在後面,我們還將看到建立在RuntimeUnbound多態基礎上的更大的進步。
然而,儘管Runtime Unbound多態具有這些優點,但因爲建立在動態語言之上,其自身存在的一些缺陷使得這項技術無法廣泛使用,並進入主流。
Runtime Unbound多態面臨的第一個問題就是類型安全。確切的講是靜態類型安全。
本質上,RuntimeUnbound多態(動態語言)並非沒有類型安全。當動態語言試圖訪問一個未知類型對象的成員時,會通過一些特殊機制或特殊接口獲得類型信息,並在其中尋找所需的對象成員。如果沒有找到,便會拋出異常。但是,傳統上,我們希望語言能夠在編譯期得到類型安全保證,而不要在運行時才發現問題。也就是說,Runtime Unbound多態只能提供運行時類型安全,而無法得到靜態類型安全。
第二個問題是性能。Runtime Unbound需要在運行時搜尋類型的接口,並執行調用。執行這類尋找和調用的方法有兩種:反射和動態鏈接。
反射機制可以向程序提供類型的信息。通過這些信息,Runtime Unbound可以瞭解是否存在所需的接口函數。反射通常也提供了接口函數調用的服務,允許將參數打包,並通過函數名調用。這種機制性能很差,基本上無法用於稍許密集些的操作。
動態鏈接則是在訪問對象前在對象的成員函數表上查詢並獲得相應函數的地址,填充到調用方的調用表中,調用方通過調用表執行間接調用。這種機制相對快一些,但由於需要查詢成員函數表,複雜度基本上都在O(n)左右,無法與動多態的O(1)調用相比。
這些問題的解決,依賴於一種新興的技術,即concept。concept不僅很消除了類型安全的問題,更主要的是它大幅縮小了兩種Runtime多態的性能差距,有望使Runtime Unbound成爲主流的技術。
concept
隨着C++0x逐漸浮出水面,concept作爲此次標準更新的核心部分,已經在C++社羣中引起關注。隨着時間的推移,concept的潛在作用也在不斷被發掘出來。concept主要用來描述一個類型的接口和特徵。通俗地講,concept描述了一組具備了共同接口的類型。在引入concept後,C++可以對模板參數進行約束:
concept assignable<T> {
T& operator=(T const&);
}
template<assignable T> void copy(T& a, T const& b) {
a=b;
}
這表示類型T必須有operator=的重載。如果一個類型X沒有對operator=進行重載,那麼當調用copy時,便會引發編譯錯誤。這使得類型參數可以在函數使用之前便能得到檢驗,而無需等到對象被使用時。
另一方面,concept參與到特化中後,使得操作分派更加方便:
concept assignable<T> {
T& operator=(T const&);
}
concept copyable<T> {
T& T::copy(T const&);
}
template<assignable T> void copy(T& a, T const& b) { //#1
a=b;
}
template<copyable T> void copy(T& a, T const& b) { //#2
a.copy(b);
}
X x1,x2; //X支持operator=操作符
Y y1,y2; //Y擁有copy成員函數
copy(x1, x2); //使用#1
copy(y1, y2); //使用#2
在靜多態中,concept很好地提供了類型約束。既然同樣是Unbound,那麼concept是否同樣可以被用於RuntimeUnbound?應當說可以,但不是現有的concept。在Runtime Unbound多態中,需要運行時的concept。
依舊使用繪圖案例做一個演示。假設這裏使用的"C++"已經支持concept,並且也支持了運行時的concept:
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
concept Shape<T> {
void T::load(xml init);
void T::draw(monitor dev);
}
...
void DrawShapes(monitor dev, vector<Shape> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<Shape> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
vg.push_back(string("xxx")); //錯誤,不符合Shape concept
...
DrawShapes(crt, vg);
乍看起來沒什麼特別的,但是請注意vector<Shape>。這裏使用一個concept,而不是一個具體的類型,實例化一個模板。這裏的意思是說,這個容器接受的是所有符合Shape concept的對象,類型不同也沒關係。當push進vg的對象不符合Shape,便會發生編譯錯誤。
但是,最關鍵的東西不在這裏。注意到DrawShapes函數了嗎?由於vector<Shape>中的元素類型可能完全不同。語句(*b).draw(dev);的語義在靜態語言中是非法的,因爲我們根本無法在編譯時具體確定(*b)的類型,從而鏈接正確的draw成員。而在這裏,由於我們引入了Runtime Unbound,對於對象的訪問鏈接發生在運行時。因此,我們便可以把不同類型的對象存放在一個容器中。
concept在這裏起到了類型檢驗的作用,不符合相應concept的對象是無法放入這個容器的,從而在此後對對象的使用的時候,也不會發生類型失配的問題。這也就在動態的機制下確保了類型安全。動多態確保類型安全依靠靜態類型。也就是所有類型都從一個抽象接口上繼承,從而將類型歸一化,以獲得建立在靜態類型系統之上的類型安全。而concept的類型安全保證來源於對類型特徵的描述,是一種非侵入的接口約束,靈活性大大高於類型歸一化的動多態。
如果我們引入這樣一個規則:如果用類型創建實例(對象),那麼所創建的對象是靜態鏈接的,也就是編譯時鏈接;而用concept創建一個對象,那麼所創建的對象是動態鏈接的,也就是運行時鏈接。
在這條規則的作用下,下面這段簡單的代碼將會產生非常奇妙的效果:
class nShape
{
public:
nShape(Shape g, int n) : m_graph(g), m_n(n) {}
void setShape(Shape g) {
m_graph=g;
}
private:
Shape m_graph;
int m_n;
};
在規則的作用下,m_graph是一個動態對象,它的類型只有在運行時才能明確。但是無論什麼類型,必須滿足Shape concept。而m_n的類型是確定的,所以是一個靜態對象。
這和傳統的模板有區別嗎?模板也可以用不同的類型參數定義成員數據。請看如下代碼:
Rectangle r;
Ellipse e;
nShape(r, 10);
nShape.setShape(e); //對於傳統模板而言,這個操作是非法的,因爲e和r不是同一種類型
動態對象的特點在於,我們可以在對象創建後,用一個不同類型的動態對象代替原來的,只需要這些對象符合相應的concept。這在靜態的模板上是做不到的。
下面的代碼則引出了另一個重要的特性:
vector<float> vFloat; //靜態對象的容器,內部存放的都是靜態對象,屬於同一類型float
vector<Shape> vShape; //動態對象的容器,內部存放動態對象,都符合Shape
同一個類模板,當使用類型實例化,執行static unbound多態;使用concept實例化,執行runtime unbound多態。兩者的形式相同。也就是說static多態同runtime多態以相同的形式表達。由於concept的加入,兩種完全不同的多態被統一在同一個模型和形式下。實際上,static和runtimeunbound多態可以看作同一個抽象體系的兩個分支,分別處理不同情況的應用。而形式上的統一,則更加接近抽象體系的本質。同時,也使得兩種unbound多態的差異被後臺化,使用者無需額外的工作,便可以同時獲得動態和靜態的抽象能力。同時,兩種多態所展示的邏輯上的對稱性,也暗示了兩者在本質上的聯繫。這裏統一的形式,便是這種對稱性的結果。
對於模板函數,則會表現出更加有趣的特性(這個函數模板有些特別,不需要template關鍵字和類型參數列表,這是我僞造的。但由於concept的使用,它本質上還是一個模板):
void draw(Shape g);
這個函數接受一個符合Shape的參數。如果我們用一個靜態對象調用這個函數:
Rectangle r;
draw(r);
那麼,就執行static unbound,實例化成一個完完整整的函數,同傳統的函數模板一樣。
如果用一個動態對象調用這個函數:
Shape g=Cycle();
draw(g);
g=Rectangle();
draw(g);
那麼,就執行runtime unbound,生成一個等待運行時鏈接的函數。上面的兩次調用,分別進行了兩次運行時鏈接,以匹配不同的動態對象。
這樣,我們可以通過函數調用時的參數對象,來控制使用不同的多態形式。更復雜的情況就是用一個函數的返回值調用另一個函數,這樣構成的調用鏈依然符合上述的調用控制原則。
下面,我們將看到Runtime Unbound多態的一個精彩表演:
//假設,我們已經定義了Rectangle、Cycle、Square、Ellipse、Trangle五個類,
// 分別map到Rectangles、Cycles、Squares、Ellipses、Trangles五個concept上,
// 這些concept都refine(可以不正確地理解爲繼承吧)自Shape。
void draw(monitor dev, Rectangles r); //#3
void draw(monitor dev, Cycles c); //#4
void draw(monitor dev, Squares s); //#5
void draw(monitor dev, Ellipses e); //#6
void draw(monitor dev, Trangles t); //#7
//此處定義一個Shape的動態對象
Shape g=CreateShapeByUserInput(); //這個函數根據用戶輸入創建圖形對象,所以圖形對象的類型只能到運行時從能確定。
draw(crt, g);
好了,現在該調用哪個版本的draw?根據用戶的輸入來。換句話說,調用哪個版本的draw,取決於CreateShapeByUserInput()函數的返回結果,也就是用戶輸入的結果。如果CreateShapeByUserInput()返回Rectangle的動態對象,那麼執行#3;如果返回的是Trangle對象,那麼執行#7。這是一種動態分派的操作。在運行時concept的作用下,實現起來非常容易。對draw的調用最終會被轉換成一個concept需求表,來自draw函數,每一項對應一個函數版本,並且指明瞭所對應的concept。動態對象上也有一個concept表,每一項存放了這個對象所符合的concept。用這兩個表相互匹配,可以找到g對象的concept最匹配的那個draw版本,然後調用。
這實際上是將重載決斷放到運行時進行,而concept在其中起到了匹配參數的作用。
這樣的做法同利用rtti信息執行類型分派調用類似:
void draw_impl(monitor dev, Rectangle& r);
void draw_impl(monitor dev, Cycle& c);
void draw_impl(monitor dev, Square& s);
void draw_impl(monitor dev, Ellipse& e);
void draw_impl(monitor dev, Trangle& t);
void draw_impl(monitor dev, Shape& g) {
if(typeif(g)==typeid(Rectangle))
draw_impl(dev, (Rectangle&)g);
else if(typeif(g)==typeid(Cycle))
draw_impl(dev, (Cycle&)g);
...
}
但是,他們卻有着天壤之別。首先,rtti分派是侵入的。如果需要增加一個圖形,需要在draw函數中增加分派代碼。而Runtime Unbound方案則只需要用新的concept重載draw函數即可。
其次,rtti版本有多少圖形類,就需要多少if...else...,而RuntimeUnbound則是一對多的。如果有幾個圖形類內容不同,但有相同的接口,符合同一個concept,那麼只需針對concept編寫一個函數版本即可。比如,如果有一個特別的CycleEx類,使用外界正方形的左上角/右下角座標描述,正好符合Ellipsesconcept,那麼只需將CycleEx map到Ellipses上即可,無需多加任何代碼。
最後,rtti需要獲取類型信息,然後做線性比較,性能無法優化。但Runtime Unbound通過concept表的相互匹配,僅牽涉數值操作,有很大的優化空間。
那麼這樣一種運行時分派有什麼好處呢?我們看到圖形類上的draw函數接受一個monitor類型參數,它代表設備。如果哪一天需要向另一種設備,比如printer,輸出圖形,那麼就需要在圖形類上增加另一個版本的draw函數。如果類是別人開發的,那麼就增加溝通的負擔。如果類是外來的,我們無法修改,那麼只能通過adapter之類的笨拙手段處理。爲了讓monitor之類同圖形本身沒有關聯的東西分離,應當使用自由函數執行draw操作。但普通函數只能接受確定的類型重載,而傳統的函數模板則限於編譯期使用,無法進行運行時分派。所以,如果能夠使用concept重載函數,並且賦予Runtime Unbound機能,那麼便可以用最簡單的形式針對一類類型進行處理,效能高得多。
運行時concept
語言層面的concept無法做到這些,因爲它是編譯期機制。爲此,我們需要有一種運行時的concept,或者說二進制級別的concept。
一個concept包含了與一個或若干個類型有關的一組函數,包括成員函數和自由函數。於是,我們就可以用一個類似“虛表”的函數指針表(暫且稱爲ctable吧)存放concept指定的函數指針。這樣的ctable依附在動態對象上,就像vtable一樣。每個對象都會匹配和map到若干個concept。因此,每個動態對象會有一個concept表,其中存放着指向各ctable的指針,以及相應的concept基本信息。
當一個“用戶”(函數或模板)需要在運行時鏈接到對象上的時候,它會提交一個concept的代碼(全局唯一)。系統用這個代碼在動態對象的concept表上檢索,獲得指向所需concept的指針,並且填寫到“用戶”給出的一個“插入點”(一個指針)中。隨後“用戶”便可以直接通過這個“插入點”間接調用所需的函數,成員或自由函數。
在這裏,concept的巧妙之處在於,將一族函數集合在一起,作爲一個整體(即接口)。那麼,在執行運行時匹配的時候,不再是一個函數一個函數地查詢,可以一次性地獲知這些函數是否存在。這就很容易地規避了類型安全保證操作的損耗。如果使用hash查詢,那麼可以在O(1)實現concept匹配。另外,一個concept的hash值可以在編譯時計算好,運行時鏈接只需執行hash表檢索,連hash值計算也可以省去。
一個動態對象可以直接用指向concept ctable的指針表示。在不同concept之間轉換,相當於改變指針的指向,這種操作非常類似OOP中的dynamic_cast。
對於如下的動態對象定義:
Shape g=Cycle();
會創建一個Cycle對象,在對象上構建起一個concept表,表中對應Cycle所有符合的concept。並且建立一組ctable,每個ctable對應一個concept。每個concept表項指向相應的ctable。而符號g則實際上是指向所建立對象的Shapesctable的指針。
對於函數:
void draw(Shape g);
draw(g);
調用g時,由於draw的參數是Shapeconcept,而g正是draw所需的concept,所以無需在對象g的concept表上匹配,可以直接使用這個ctable指針。這就是說,只要所用動態對象(g)的concept同使用方(draw函數)能夠匹配,便可以直接使用指向ctable的指針鏈接(編譯時鏈接),無需在運行時重新匹配。只有發生concept轉換時,才需要在concept表中搜索,獲得所需的ctable指針:
Swappable s=g; //Swappable是另一個concept
這種情況同dynamic_cast極其相似。也可以模仿着採用concept_cast之類的操作符,使得concept轉換顯式化,消除隱式轉換的問題(強concept化)。
Runtime Unbound和Runtime Bound
對於runtime unbound同runtime bound之間的差異前面已經有所展示。在其他方面,兩者還存在更多的差別。首先,就像繪圖案例中展示的那樣,runtime unbound是非侵入的。runtime unbound不要求類型繼承自同一類型,只需將類型同concept關聯起來便可。
其次,concept不是一種侷限於OO的技術,不僅涉及成員函數,還包括了自由函數,範圍更廣,更加靈活。
最後,實現上,Runtime Unbound和RuntimeBound之間有驚人的相似之處。兩者都採用一個函數指針表作爲操作分派;都採用一個指向函數表的指針作爲入口;一個動態對象上的concept之間的轉換,也同動多態對象一樣,在不同的函數表間切換。他們唯一的不同,是實現接口的機制。
動多態用類型兼任接口,通過繼承和虛函數實現接口的功能。用類型作爲類型的接口,使得這兩個本來獨立的概念交織在一起。增加整個類型體系的複雜度和耦合度。 concept則利用獨立的系統描述、表達和管理接口。類型則迴歸到表達業務對象的功能上來。
動多態在使用類型表達接口的時候,便很容易地引入一個麻煩的問題,表達功能的類型和表達接口的類型混合在一起,使用時必須通過一些方法區分出哪些是接口,哪些是功能類型。這增加了對象模型的複雜性。而concept則獨立於類型體系之外,所有對接口的操作都是單一的,檢索和匹配來得更加方便快捷。
作爲繼承體系的基礎部分,動多態的抽象接口必須在繼承結構的最頂端。那麼這些抽象類型必須先於其他類型出現。這對系統的早期設計產生很大的壓力,往往一個基礎抽象接口設計有誤,便會造成整個體系的變更。
而concept是獨立於類型的,那麼任何時候都可以將一個類型同接口綁定。接口甚至可以在類型體系基本建立之後才確定。這種靈活性對複雜軟件的開發至關重要,去掉了長期以來套在人們頭上的枷鎖。
前面已經提到,在不需要concept轉換的情況下,無需執行運行時的concept匹配,所有的調用具有同動多態一樣的效率(都是間接調用)。在執行concept轉換時,無需象動多態那樣在複雜的繼承體系上檢索,只需執行concept表的hash匹配,效率反而更高,而且更簡單。考慮到這些情況,我們可以認爲concept化的Runtime Unbound多態完全能夠替代傳統的動多態。也就是說,我們不再需要動多態了。
想象一下,如果一門語言能夠擁有運行時concept,那麼它完全可以只保留Static Unbound和RuntimeUnbound多態,而放棄RuntimeBound多態。一旦放棄動多態(沒有了虛函數和虛表),那麼對象模型便可以大大簡化。所有對象只需要線性分佈,基類和成員依次堆疊在一起,也沒有vtable的干擾,對象結構可以做到最簡單。同時,繼承也迴歸了代碼重用的傳統用途。而且,對象獨立於接口存儲,在能夠在編譯時靜態鏈接的時候,可以作爲靜態對象使用。而在需要動態對象的地方,又可以很容易地轉換成動態對象,只需要爲其附上concept表和ctable。一切都簡化了。對象模型也更加容易統一。
這對於很多底層開發的程序員對於c++複雜而又混亂的對象模型難以接受。如果能夠廢除虛函數,簡化對象模型,那麼對於這些底層開發而言,將會帶來直接的好處。只要確保不使用concpt定義對象、實例化模板,便可以使整個軟件執行StaticUnbound。這相當於去掉OOP的C++。否則,就啓用Runtime Unbound,實現運行時多態。
總結
Static Unbound和RuntimeUnbound作爲一對親密無間的多態技術,體現了最完善的抽象形式。兩者各踞一方,相互補充,相互支援。而且兩者具有統一的表現形式,大大方便了使用,對於軟件工程具有非凡的意義。另一方面,RuntimeBound多態作爲OO時代的產物,體現了靜態類型語言在運行時多態方面的最大努力。但是,隨着運行時concept的引入,RuntimeUnbound多態自身存在的靜態類型安全問題和性能問題,都能夠得到很好的解決。至此,Runtime Unbound便具備了替代RuntimeBound的實力。相信在不久的將來,Runtime Bound將會逐漸步入它的黃昏。參考
- http://groups.google.com/group/pongba/web/Runtime+Polymorphic+Generic +Programming.pdf。大牛人Jaakko Järvi等寫的關於Runtime concept的文章,講解了runtime concept的概念的實現方法,並在ConceptC++上以庫的形式實現。其中使用傳統的動多態實現runtime concept,這表明動多態的實現機制同runtime concept是一致的。當然庫的實現很複雜,這是“螺螄殼裏做道場”,無奈之舉。Runtime concept還是應當在語言中first-class地實現。
- http://www.lubomir.org/academic/MinimizingCodeBloat.pdf。也是Jaakko Järvi寫的,運行時分派的文章。
- http://opensource.adobe.com/wiki/index.php/Runtime_Concepts。
- Inside C++ Object Model。
附錄 Runtime Concept的具體實現
我們有一個concept:concept Shape<T>
{
void T::load(xml);
void T::draw(device);
void move(T&);
}
另外,還有一個代表圓的concept:
concept Cycles<T> :
CopyConstructable<T>,
Assignable<T>,
Swappable<T>,
Shape<T>
{
T::T(double, double, double);
double T::getX();
double T::getY();
double T::getR();
void T::setX(double);
void T::setY(double);
void T::setR(double);
}
現在有類型Cycle:
class Cycle
{
public:
Cycle(double x, double y, double r);
Cycle(Cycle const& c);
Cycle& operator=(Cycle const& c);
void swap(Cycle const& c);
void load(xml init);
void draw(device dev);
double getX();
double getY();
double getR();
void setX(double x);
void setY(double y);
void setR(double r);
private:
...
};
當定義一個動態對象:
Shape g=Cycle();
便會形成如下圖的結構:
如果遇到語句:
Swappable h=concept_cast<Swappable>(g);
那麼,將會執行一個搜索,用conceptSwappable的id(比如hash碼)在concept表中檢索是否存在Swappable項。如果存在,就將對應項的指針賦給h。這種操作同dynamic_cast操作非常相似,只是相比在複雜的對象結構中查詢更加簡單迅速。
concept表置於對象的頭部或尾部,這是爲了便於對象檢索concept接口。每個類型的ctable只需一份。
對象本體可以很容易地同concept表分離,在完全靜態的情況下,concept表是不需要的。如果需要runtime多態,加上concept表即可。
Feedback
偶然看到
受益匪淺!
自己模擬了一下
http://blog.csdn.net/cchhope/archive/2008/04/18/2304969.aspx