《C++0x漫談》系列之:Concept, Concept!

C++0x漫談》系列之:Concept, Concept!

 

By 劉未鵬(pongba)

 

C++0x漫談》系列導言

 

這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看着各個重要proposals一路review過來:rvalue-referencesconceptsmemory-modelvariadic-templatestemplate-aliasesauto/decltypeGCinitializer-lists…

 

總的來說C++09C++98相比的變化是極其重大的。這個變化體現在三個方面,一個是形式上的變化,即在編碼形式層面的支持,也就是對應我們所謂的編程範式(paradigm)C++09不會引入新的編程範式,但在對泛型編程(GP)這個範式的支持上會得到質的提高:conceptsvariadic-templatesauto/decltypetemplate-aliasesinitializer-lists皆屬於這類特性。另一個是內在的變化,即並非代碼組織表達方面的,memory-modelGC屬於這一類。最後一個是既有形式又有內在的,r-value references屬於這類。

 

這個系列如果能夠寫下去,會陸續將C++09的新特性介紹出來。鑑於已經有許多牛人寫了很多很好的tutor這裏這裏,還有C++標準主頁上的一些introductiveproposals,如這裏,此外C++社羣中老當益壯的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重複勞動了:),我會盡量從一個宏觀的層面,如特性引入的動機,特性引入過程中經歷的修改,特性本身的最具代表性的使用場景,特性對編程範式的影響等方面進行介紹。至於細節,大家可以見每篇介紹末尾的延伸閱讀。

 

Concept

 

好吧好吧,我承認我跳票了,上次說這次要寫variadic templates的。但g9老大寫了一篇精彩的散記,讓我覺得concept應該先寫,因爲這實在是個有意思的特性,比variadic templates有意思多了。

 

我和Concept不得不說的事

事兒#1

看看下面這坨代碼有什麼問題:

 

std::list<int> li;

std::sort(li.begin(), li.end());

 

如果對人肉編譯不在行的話,可以用你手頭的編譯器試一下。你會發現,你的編譯器一碰到這簡單而無辜的兩行代碼便會一反常態,跟個長舌婦似的吐出一大堆&#$@*^,令人牙酸的錯誤信息來。在使用C++模板庫時這種編譯錯誤井噴是家常便飯,動輒噴出令人應接不暇的4K字節的錯誤信息出來。你還以爲不是編譯器井噴,而是你自己RP井噴了,於是一臉無辜地跑去問模板達人,後者擡了擡眼皮,告訴你說“把list改成vector因爲listiterator不是random的而std::sort需要randomiterator”,你一邊在腦子裏給這句話分詞加標點符號一邊想弄明白他是怎麼從一堆毛線似的字符裏抽象出這麼個結論的。

 

實際上,這個問題比你想像得嚴重,其根本問題在於降低工作效率,你得在你本不需要花工夫的地方(人肉解析編譯錯誤)花工夫;這個問題比你想像得普遍,乃至於居然有人把“能夠獨立地解決所有的編譯與鏈接問題”也列在了“有實際開發工作經驗”要求裏面;這個問題比你想像得影響惡劣,因爲你可以想像可憐的新手在兩行貌似無辜的代碼面前哭喪臉的模樣——C++編譯器就這樣把一個可憐的潛在C++用戶給扼殺了。你也可以想像爲什麼有那麼多人不喜歡C++模板——其實語法只是其一個非主要的方面。

 

實際上你請教的那個達人並沒有什麼火星抽象能力,只不過是喫過的橋比你走過的鹽還多而已。而這,還預示着另一個問題,就是能人肉解析模板編譯錯誤居然也成爲了衡量C++達人與否的一個標準不信你去各個罈子上轉一轉看看有多少帖子是詢問關於編譯錯誤的問題的,其中又有多少是關於模板編譯錯誤的。

 

更小概率的是居然還存在一個專門解析STL相關錯誤信息的“STL錯誤解碼器”——STLFilt。這玩意幫你把編譯錯誤轉換成人能識別的自然語言,不錯是不錯。可惜STLFilt有了,BoostFilt呢?ACEFilt呢?我自己寫的模板庫呢?

 

其實,造成這個問題的直接原因是C++的類型系統的抽象層次太低。C++的靜態強(也有人說C++的類型系統其實是弱類型系統,anyway)類型系統所處的抽象層面是在基本類型(intdoublechar…)層面的。一方面,C++雖然擁有對自定義類型的上乘支持(比如,支持將自定義類型的接口裝扮得跟內建類型幾乎毫無二致——vector vs. build-in array),然而另一方面,C++的類型系統卻對於像vector這樣的抽象從語意上毫不知情。直接的後果就是,一個高層的類型錯誤往往以相差了十萬八千里的底層類型錯誤表現出來,結果就是你得充當一次福爾摩斯,從底層錯誤一直往上回溯最終找到問題的發生點。譬如一開始給出的那個例子:std::sort(li.begin(), li.end());的錯誤,如果C++類型系統的抽象層能高一些的話(所謂抽象層次高,就是知道高層抽象概念(Concept)的存在,如“隨機迭代器”這個概念),給出的錯誤無非就是:“list的迭代器不滿足隨機迭代器這個概念(concept)的要求(requirements)”。然而由於C++並不知道所謂concept的存在,所以問題到它眼裏就變成了“找不到匹配的operator+…”一堆non-sense

 

事兒#2

大二上學期的時候我們上一門計算方法的課程,期末考試要寫一些矩陣算法。地球上的程序員大抵都知道矩陣算法不用Matlab算基本等於沒事找抽,一大堆accidental complexities在那恭候着,一個index錯誤能讓你debug到抓狂。當時我C++用得半斤八兩,模板七竅也差不多通了六竅;爲了到上機考試的時候節省點時間,就事先寫了一個簡單的矩陣庫,封裝了一些基本的操作和像高斯消元這種基本算法。

 

那個時候你能指望我知道TDD?還是XP?或者STLLint?於是呢?寫了一個簡單的程序,簡單使用了一下寫好的庫,發現編譯通過後就興沖沖地告訴哥們說:大家不用怕,有我這Matrix庫罩着,寫算法跟寫僞碼差不到哪去!

 

兩天後上機考試,程序不同了,等於測試用例不同了,結果原來沒有出現的編譯錯誤一下統統跑出來了。原來爲什麼不出現?一個原因是原來有些成員函數就沒用到,C++說,在一個模板類裏面,沒用到的成員函數是不予編譯的。那不予編譯就代表不予糾錯嗎?不予類型檢查嗎?令人悲傷的是,的確如此。或者把置信度提高一點說,幾乎如此。爲什麼?看看下面的代碼:

 

template<typename T>

void f(T& t)

{

t.m();

}

 

你說編譯器看着這個函數,它怎麼做類型檢查?它怎麼知道t上面有沒有成員函數m?它連t的類型都不知道。“很久很久以前,模板就是這樣破壞模塊式錯誤檢查的

 

實際上,C++98那會,爲了能夠儘早儘量檢查模板代碼中的隱患,以響應“防範勝於救災,隱患重於明火”的號召,C++甚至將模板上下文中的代碼中的名字生生分成了兩類,一類叫dependent names,一類叫undependent names。舉個例子,上面那段代碼中的m成員函數就是dependent的,因爲它的隱含this參數t的類型是dependent的;對於dependent name,不作類型檢查——原因剛纔講過,因爲類型信息根本就沒有。剩下的就是undependent names了,比如:

 

void g(double); // #1

 

template<typename T>

void f()

{

g(1);

}

 

void g(int); // #2

 

int main()

{

f<int>();

}

 

這裏f裏面調用的g綁定到哪呢?答案是#1。因爲g是個undependent name(雖然它位於模板函數(上下文)裏面)。而對於undependent name,還是趕緊進行類型檢查和名字綁定吧,有錯誤的話也能早點暴露出來,於是g便在它的使用點“g(1)”處被查找綁定了——儘管#2處的g(int)是一個更好的匹配,但在g(1)處只有g(double)是可見的,所以g(double)被編譯器看中了,可憐的g(int)只能感嘆“既生g(int),何生g(double)…”。

 

這,便是臭名昭著的腰斬…sorry…是二段式名字查找,C++著名的複雜性來源之一。說它臭名昭著還有一個原因——在衆多編譯器支持良莠不齊的C++複雜特性中,它基本可以說是位居第一(第二估計要留給友元聲明瞭),VC掙扎到8.0還是沒有實現二段式名字查找,而是把所有的工作留到模板實例化點上進行,結果就是上面的例子中會選中#2

 

D&E中對此亦有詳細介紹。

 

實際上,這個二段式名字查找的種種問題正從一個側面證明了早期類型檢查是何等重要,動態語言的老大們在Ruby翻出來的舊瓶新酒Duck Typing吵翻了天其實說的也是這個問題(sorry,要加上“之一”)。

 

事兒#3

在一個無聊的午後,我在敲打一坨代碼,這是一個算法,算法要用到一個容器,算法是用模板來實現的:

 

template<typename ContainerT>

void XXXAlgo(ContainerT cont)

{

… cont.

 

在我敲打出“cont”加點號“.”之後,我習慣性地心理期待着“智能”的IDE能夠告訴我cont上面有哪些成員函數,正如我們每次敲打出“std::cout.”之後一樣。習慣成自然,你能說我不對麼?難道你金山餈粑用久了不也一樣在讀影印版紙書遇到不認識單詞的時候想着把手指頭伸過去指着那個單詞等着跳出個詞條窗口來?難道只是我?咳咳

 

問題是,我知道XXXAlgo的那個模板參數ContainerT是應當符合STLContainer概念(concept的,我當然希望編譯器也能知道,從而根據Container概念所規定它必須具有的成員函數來給我一個成員函數列表提示(beginendsize…),難道這樣的要求很過分嗎?它沒有道理很過分啊,覺得它很過分我會說的啊,不可能它明明不過分我偏要說它很過分,他很過分我偏要說它不過分啊你覺得這要求過分你就說嘛亂敲鍵盤是不好滴,鍵帽掉下來砸到花花草草也不好啊你看,“.”鍵又給你磨平了

 

一方面,程序員一臉無辜地認爲IDE應該能夠看到代碼裏面的ContainerT暗示着這是一個符合STLContainer概念的類型。而另一方面IDE廠商卻也是理直氣壯:寫個ContainerT就了不起啊,萬一遇到個C過來的,寫成ContT我怎麼辦?寫成CntnrT哪?再說你覺得ContainerT是對應STLContainer概念的,別人還用這個單詞來對應線程池呢怎麼辦捏?什麼?他不知道“poor”怎麼寫管我啥事嘞?我身爲一個IDE,根據既有的信息,作出這樣的假設,既合情也合理

 

事兒#4(此事純虛虛構,如有巧合,算你運氣背)

一天,PM跑過來告訴你說:“嘿,猜怎麼着,你寫的那坨模板代碼,隔壁部門人用了說很不錯,希望你能把代碼和文檔完善一下,做成一個內部使用的庫,給大家用,如何?”你心頭一陣花枝亂顫:“靠!來部門這麼久了,C++手段終於可以展露一下了。”於是廢寢忘食地按照STL文檔標準,遵照C++先賢們的教誨,寫了一個漂漂亮亮的文檔出來。裏面Concept井井有條,Requirements一絲不苟

 

動態語言的老大們常掛在嘴邊的話是什麼?——需求總是在變的。又一天,你發現某個Concept需要revise了,比如原來的代碼是這樣的:

 

template<typename XXX>

void f(XXX a)

{

 

a.m1();

}

 

本來XXX所屬的那個Concept只要求有m1成員函數。後來因需求變更,XXX上需要一個新的成員函數m2。於是你的代碼變成了:

 

template<typename XXX>

void f(XXX a)

{

 

a.m1();

a.m2();

}

 

但僅改代碼是不行的,文檔裏面關於XXX所屬的那個Concept的描述也要同步修改可惜天色已晚,良宵苦短,你準備睡一覺明天再說結果第二天一早你就被boss叫去商量一個新的項目(因爲你最近表現不錯),於是你把這事給忘了。於是跟代碼不一致的文檔就留在那裏了

 

這種文檔和代碼不一致的情況太常見了,根本原因是因爲代碼和文檔是物理上分離的,代碼不能說謊,因爲要運行,但文檔呢?什麼東西能驗證文檔精確反映了代碼呢?除了往往忽視文檔的程序員們之外沒有其他人。這個問題是如此廣泛和嚴重以至於程序員們乾脆就近乎鴕鳥式地倡導“代碼就是文檔”了,這句話與其說是一個陳述句,不如說是一個美好的願景(遠景?)。

 

好吧,好吧,你記性好,這點小事你不會忘掉,第二天一早你就把文檔給改了,你真是勞模。可惜過一天,需求居然又改變了(你心說是哪個傢伙負責客戶需求分析的?!),這下你需要修改Concept繼承體系了

 

你看,可能造成文檔和代碼脫節的因素太多了,一般一段時間以後,能說得上話的也就剩代碼,文檔只能拿來看看“系統應該是什麼樣子的”,只有代碼才陳述了“系統實際是什麼樣子的”。

 

然而,如果文檔就在代碼當中呢?不,我不是說註釋,你又不是不知道要寫出合格的註釋比寫出合格的小說還要難。我是說,代碼就是文檔文檔就是代碼

 

此外,把Concept約束寫在代碼裏面還有一個好處就是能夠使得被調用函數和調用方之間的契約很明顯,Concept的作用就像門神,告訴每一個來調用該函數的人:“你要進去的話必須滿足以下條件”。RubyDuck Typing被詬病的原因之一就是它的Concept在代碼裏面是隱式的,取決於對象上的哪些方法被使用到了。

 

事兒#5

重構重不重要Martin Fowler叔叔笑了

 

原來我抽屜裏有這麼一段代碼:

 

template<typename XXXConcept>

void foo(XXXConcept t)

{

t.m1(); // #1

}

 

template<typename XXXConcept>

void bar(XXXConcept t)

{

t.m1(); // #2

}

 

現在我想對代碼作一種最簡單的重構——改名。m1這個名字不好聽,我想改成mem。於是我指望編譯器能替我完成這個簡單的任務,我把鼠標指到#1處,在m1上右擊,然後重命名m1mem。同時很顯然我期望“智能”的編譯器能夠幫我把#2處也改過來,因爲它們用的是同一個concept上的成員函數。

 

但編譯器不幹,原因見事兒#3。或者見這篇blog,後者舉了一個類似的例子——如果我們重命名實現了那個XXXConcept的類上的m1方法,那麼#1#2處的調用能被自動重命名嗎?Ruby Refactoring Browser的作者笑了

 

事兒#6

很久很久以前我寫了一個容器類。這個容器類裏面該有的功能都有了唯一的問題是,當時我還不知道STL(準確地說是就算知道也沒用),結果呢?這個各方面功能都完備的容器類的使用界面(接口)並不符合STL容器的規範。比如我把begin()叫做start(),把end()叫做還是叫做end()(不然總不能叫finish()吧?)。我還把empty()叫做isEmpty()而另一方面我的empty()實際卻做的是clear()的工作

 

後來,我又寫了一個算法,這個算法是針對STL容器的,你問我幹嘛不針對迭代器編程?很簡單,因爲我要用到emptyfront/backclear等成員函數。基於迭代器編寫也不是不行,就是得再費一袋煙此外還有兩個問題,一是效率,而是影響算法使用說到效率

 

現在,我想讓我的這個算法也能操縱我原來那個古董容器(我不是指我家那個慈禧尿壺),但因爲那個古董容器的接口跟我的算法要用到的接口不一致:

 

class MyCont { … void isEmpty(); … };

 

template<typename Cont>

void f(Cont cont){ … cont.empty(); … }

 

怎麼辦?修改MyCont的實現?可以,因爲這個MyCont是我寫的,後者意味着兩點:一,我有權修改它。二,我寫的庫沒其他人用。可是如果MyCont是位於另一個庫當中的呢?如果有一堆依賴於它的既有代碼呢?

 

或者,寫個wrap類?還是太麻煩了,況且wrap手法也不是沒有自己的問題。我們只不過想適配一下接口而已。

 

其實,我們只想對編譯器說一句:MyContisEmpty其實就是empty,您行行好就放行吧

 

事兒#7

如果一個生物走起路來像個火星人,

說起話來像個火星人,

回起貼來像個火星人,

那他肯定就是火星人。

——火星人判別最高綱領

 

Ruby的串紅使火星人類型系統煥發出了第二春。

 

Rubyers的口號是,我不關心你是不是真的是火星人,看你丫的回帖像剛從火星迴來的,你一定就是火星人!

 

Sorry,用嚴肅一點的話來說,就是“不關心一個對象的具體類型,而只關心一個對象的行爲”。用鎬頭書上的例子就是:

 

class Customer

def initialize(first_name, last_name)

@first_name = first_name

@last_name = last_name

end

def append_name_to_file(file)

file << @first_name << " " << @last_name

end

end

 

file << @first_name,這裏file並不一定要是真正的文件,而只要是一個支持“<<”操作的對象即可(想起C++的流插入符了嗎?)。所以要測試這個Customer類的append_name_to_file,也就不一定要真正創建一個文件出來傳給它當參數,只要傳一個支持<<操作的對象給它就可以了——比如一個String對象。用String對象的好處就是檢查被寫入到這個String裏面的東西很容易,而用File對象的話還得開文件關文件的,麻煩。

 

事實證明Ruby的火星人類型系統是非常靈活的。鎬頭書上還舉了另一個實際的例子:有這麼一坨代碼,它遇到數據量大的時候就變得奇慢,項目期限在即,當花了一點時間檢查問題所在之後,發現問題在於代碼中創建了許多的String臨時對象,而速度問題則是因爲GC運行起來了,之所以有這麼多String臨時對象是因爲一個循環裏面不斷往一個String對象上面Append新子串,導致原來的串對象被丟棄,一地雞毛。

 

結果還是火星人類型系統“to the rescue”。通過僅改變一兩行非關鍵代碼,該項目得救了。作出的改變就是把那個String對象換成一個Array,這樣每次往上面Append新串的時候都會把這個新串當作一個新的元素掛到這個Array對象上,活像一串串臘肉;由於沒有舊串被丟棄,因此也就不會出現遍地垃圾的情況。

 

好吧,我承認我在練習中學語老師教的欲抑先揚手法。不過Ruby Fans大可不必激動,因爲兩個原因:一,C++也有同樣的問題,所有的模板代碼用的本質上也都是火星人類型系統(C++98只支持完全unconstrained templates)——管你實際上是不是迭代器,只要你有++--*->等操作就行。二,火星人類型系統的危險是理論上的,實際上誰也沒有案例證明它導致了什麼災難。比如在Cedricblog上這篇“The Perils of Duck Typing”後面就有人跟貼說一個JarFile上有一個explode(解壓)和一個NuclearBomb上有一個explode(爆炸),於是你的算法把一個核彈給“解壓”(爆炸)了。從這個例子的極端也不難看出其實這種危險的可能性並不大。有一次我在新聞組上發帖,扯到這個問題上,也有人舉了一個例子,說手指有一個方法叫“插”(Plug),而插頭也有一個方法叫“插”(Plug),於是不管三七二十一的算法就面臨把一根手指(誰的誰倒黴)插到插座中去的危險。

 

這些例子說到底都有點飄逸,不切實際。具體的例子你問我我也沒有,或許C++裏面倒是可以捏造出一個“比較”實際一點的來:

 

template<typename StreamT>

void f(StreamT& stream) { stream << 1; }

 

int i;

f(i);

 

這段代碼編譯器也樂呵呵地編譯了,因爲整型是支持<<(位移)操作的。這裏的錯誤很顯然,但若是藏在成千上萬代碼當中,因爲一個打字錯誤而漏進去的話,也許就不那麼顯然了。

 

從本質上說,火星人類型系統是將語法結構的同一性視爲語意層面的同一性(即所謂的Structural Conformance);這纔是它的根本問題。而另一方面,傳統的接口繼承(即所謂的Nominal Subtyping)則更嚴格:當你繼承自一個接口的時候,你明確而清醒地知道你是要實現該接口所表達的抽象(語意),你不會“一不小心”(accidentally)實現了一個接口的,你必須寫上“implements …Java/ public … C++)”幾個大字母纔行。

 

Concept to the rescue

話說到這份上如果你還不知道我要說什麼那我就繼續說吧

 

以上七個問題一直都是GP中被廣爲爭論的問題,其中duck typing#7)在動態語言社羣爭論得比在C++裏面還要激烈得多;同時它們也都是由來已久的問題,有的甚至久遠到BjarneD&E中就已經遇見到了,只是當時C++標準化的進度太緊來不及解決而已,這一晃就是10

 

沒錯,它們全部都可以用Concept來漂亮地解決。或者換個說法,Concept的出現就是爲了解決以上七個問題的——

 

#1(編譯錯誤問題)——有了ConceptC++的類型系統抽象層次便提高了一個級別,在遇到編譯錯誤的時候便能說“XXX不滿足XXXConcept”這樣的話了。

 

#2(模塊式類型檢查問題)——有了Concept,原本所謂的unconstrained templates便可以做成constrained。比如STLfor_each算法就變成了這樣:

 

template<InputIter Iter, Callable1 Fun>

Fun for_each(Iter first, Iter last, Fun func)

{

  for(;first != last; ++first) func(*first);

  return func;

}

 

其中InputIteratorCallable1都是Concepts

 

auto concept Callable1<typename F, typename X> {

  typename result_type;

  result_type operator()(F&, X);

};

 

concept InputIterator<typename X> : EqualityComparable<X>, …

{

 

  pointer operator->(X);

  X& operator++(X&);

  postincrement_result operator++(X&, int);

  reference operator*(X);

}

 

有了這些Concept,編譯器在對for_each作類型檢查的時候便能夠往InputIterator/Callable1裏面進行名字查找:“first != last”可不可行?只要看first的類型支不支持“!=”,那first的類型支不支持“!=”呢?因爲first的類型Iter是滿足concept InputIterator的,那就只要看InputIterator裏面有沒有“!=”就行了,有嗎?沒有?哦,不好意思,忘記說了,InputIterator是繼承自EqualityComparable的,後者裏面定義了“!=”。

 

auto concept EqualityComparable<typename T, typename U = T> {

bool operator==(T a, U b);

bool operator!=(T a, U b) { return !(a == b); }

}

 

同樣的,“++first”這個表達式可行嗎?只要看first的類型支不支持“++”就可以了,後者只要看InputIterator這個concept支不支持“++”,答案是支持。

 

#3IDE智能提示問題)——編譯器既然知道了concept的存在,當你敲下iterctrl+空格的時候編譯器便能夠通過解析InputIterator這個concept的定義來告訴你iter對象支持哪些操作了。

 

#4(文檔代碼分離問題)——瞧一瞧for_each的聲明,原來(C++98)是:

 

template<typename InputIterator … >

void for_each(InputIterator iter …);

 

現在(C++09)是

 

concept InputIterator

{

}

 

template<InputIterator Iter … >

void for_each(Iter …);

 

區別在什麼地方?原來的代碼中沒有concept InputIterator這樣的聲明,你看着InputIterator這麼個單詞,得去STL的文檔裏面查才知道它到底有那些requirements。有了concept之後呢?只要翻開InputIterator這個concept的定義就看到了,後者將位於C++09<iterator>頭文件中。

 

#5(重構問題)——重構?當然!有了concept,要重構的時候只要修改concept定義,所有使用了該concept內的函數的地方都可以容器地作出改變。

 

#6(接口適配問題)——實際上前面“事兒#6裏面提到的古董容器+非古董算法的例子雖然能說明問題,但總是不夠巧妙。還不如直接抄Concept六君子OOPSLA ‘06上的牛paper中的例子,Douglaspaper裏面舉了一個圖論庫的例子:有一個矩陣算法,但另外還有一個圖(Graph),就算沒喫過圖總見過圖走路吧——圖是可以用矩陣來表示的,所以只要用concept_map把圖類的接口適配一下就可以拿那個現成的矩陣算法來操縱了,如下:

 

template<Graph G>

concept_map Matrix<G>

{

  typedef int value_type;

  int rows(const G& g) { return num_vertices(g); }

  int columns(const G& g) { return num_vertices(g); }

  double operator()(const G& g, int i, int j)

  {

    if (edge_e = find_edge(ith_vertex(i, g), ith_vertex(j, g), g))

      return 1;

    else return 0;

  }

};

 

#7(火星人類型系統問題)——火星人的危險性前文已經闡述了。其危險在於將語法結構同一性視爲語意同一性。用傳統的接口繼承就沒有這個問題,因爲當你繼承自一個接口的時候,你明確知道你在幹嘛——實現這個接口的語意。因此,在C++09concept中,缺省的concept是“非auto”的,也就是說:

 

concept Drawable<typename T>

{

  void T::draw() const;

}

 

class MyClass

{

  void draw() const;

};

 

這種情況下MyClass是不會自動滿足Drawable這個concept的(儘管它的確實現了一個一模一樣的draw()函數),這是爲了避免無意間實現了一個不該實現的concept。要想讓MyClass實現Drawable這個concept,必須顯式地說明這一點:

 

concept_map Drawable<MyClass> { }

 

是不是看上去很像模板特化?實際上concept的內部編譯器實現正是利用既有的一套模板特化系統來進行的。

 

但是如果每個concept都要靠concept_map來支持的話太麻煩,有些基本的concept比如EqualityComparable——只要一個類型重載了operator==,那麼就肯定是EqualityComparable的。這個論斷幾乎肯定是安全的,因爲沒有誰會不知道operator==的語意吧?所以,EqualityComparableauto的:

 

auto concept EqualityComparable<typename T, typename U = T> {

bool operator==(T a, U b);

bool operator!=(T a, U b) { return !(a == b); }

}

 

這樣一來如果你的類實現了operator ==,你不需要將它concept_mapEqualityComparable,就能自動(auto)實現EqualityComparable;一句話,回到原始的“結構一致性”上面。

 

關於auto的另一個作用,我想到了一個絕妙的介紹,但這裏空白太小寫不下了,請聽下回分解:-)

 

延伸閱讀

[1] Concepts: Linguistic Support for Generic Programming in C++

此篇是Concepts的權威飼養指南,高屋建瓴鉅細靡遺地介紹了Concept的方方面面。

[2] An Extended Comparative Study of Language Supports for Generic Programming

此篇對各種語言對GP的支持做了極其詳盡的survey,其中也提到了concept的一些東西,很有價值的一篇paper

[3] Concept checking – A more abstract complement to type checking

當年,C++的老豆率先發難,寫了這篇最早的concept paper,其間對三大實現策略作了高屋建瓴的比較,對掌握concept的本質有非常好的幫助。

[4] Concepts

一番刀光劍影你來我往之後,user-pattern派(由Bjarne本人發起)和function signature派(由Douglas帶領)終於聯合起來;這是第一篇署名Bjarne Stroustrup & Douglas GregorConcept ProposalFunction Signature的做法被正式確定下來(主要原因之一是它提供了#6(類型適配)這個大大的好處)。

[5] Proposed Wording for Concepts(rev#1)

這個就不用說了,截止到最近的concepts標準提案。

[6] Concepts for the C++0x Standard Library Utilities(rev#2)

這個自然也不必說了,C++0x標準庫裏面的一些基本的concepts定義。

[7] http://www.generic-programming.com

ConceptGCC的官方站,含GCC實現的下載,以及歷屆concepts相關paper

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章