其實(shí)我在寫(xiě)這個(gè)系列的第三篇文章的時(shí)候就已經(jīng)發(fā)現(xiàn),距離機(jī)器越遠(yuǎn),也就是抽象越高的概念,坑的數(shù)量是越少的。但是這并不是說(shuō),距離機(jī)器越近的概念就越強(qiáng)大或者說(shuō)越接近本質(zhì)。這是廣大的程序員對(duì)計(jì)算理論的一種誤解。大多數(shù)人理解編程的知識(shí)結(jié)構(gòu)的時(shí)候,都是用還原論來(lái)理解的,這個(gè)方法其實(shí)并沒(méi)有錯(cuò)。但問(wèn)題在于,“還原”的方法并不是唯一的。很多人覺(jué)得,反正你多高級(jí)的語(yǔ)言編譯完了無(wú)非都是機(jī)器碼嘛。但是還有另一種解釋?zhuān)銦o(wú)論多低級(jí)的語(yǔ)言編譯完了無(wú)非也就是帶CPS變換(continuation passing style)的λ-calculus程序嘛。他們是等價(jià)的,不僅能力上也是,“本質(zhì)”上也是。
一個(gè)用CPS變換完整地處理過(guò)的λ-calculus程序長(zhǎng)的就很像一串指令。而且類(lèi)似于C++的inline操作,在這里是完全自然、安全、容易做的。那其實(shí)為什么我們的機(jī)器不發(fā)明成這樣子呢?顯然這完全跟我們想如何寫(xiě)一個(gè)程序是沒(méi)關(guān)系的。正是這種沖突讓我們有一種“概念距離機(jī)器越遠(yuǎn)運(yùn)行速度就越慢”的錯(cuò)誤的直覺(jué)。扯遠(yuǎn)了講,就算你在用一門(mén)函數(shù)式語(yǔ)言,譬如說(shuō)Haskell也好,F(xiàn)#也好,最終在運(yùn)行的時(shí)候,還是在運(yùn)行徹底編譯出來(lái)的機(jī)器碼。這些語(yǔ)言是完全不需要“模擬器”的,雖然由于各種歷史原因人們首先開(kāi)發(fā)了模擬器。當(dāng)然一個(gè)精心設(shè)計(jì)過(guò)的C程序肯定還是要比haskell快的,但是我覺(jué)得能這么干的人不多,而且大多數(shù)時(shí)候這么干都是在浪費(fèi)老板的錢(qián)而已,因?yàn)槟銈兊某绦蛟揪筒恍枰斓侥欠N份上。這種東西就跟那些做互聯(lián)網(wǎng)對(duì)于測(cè)試的想法是一樣的——有bug?發(fā)現(xiàn)了再說(shuō),先release搶市場(chǎng)。
如果對(duì)這方面有了解的話(huà),CPS變換——也就是Lost In Stupid Parentheses-er們最喜歡的call-with-current-continuation,他的另一個(gè)名字叫call/cc——是一種跟goto一樣強(qiáng)大而且基本的控制流的做法。goto和CPS可以互相轉(zhuǎn)換不說(shuō)了,所有其它控制流都可以轉(zhuǎn)換成goto和CPS。它們兩者在這方面是不相上下的。而且既然一個(gè)完全用CPS變換處理過(guò)的程序長(zhǎng)得就像一串指令,那你說(shuō)他們的區(qū)別是什么呢?區(qū)別就是,CPS可以是強(qiáng)類(lèi)型的,而goto則永遠(yuǎn)都不可能。
作為廢話(huà)的最后一段,我給個(gè)小例子來(lái)講什么叫“一個(gè)用CPS變換完整地處理過(guò)的λ-calculus程序長(zhǎng)的就很像一串指令”。就讓我們用a(b( x ), c( x ))這樣的一個(gè)表達(dá)式來(lái)講:
處理前:
處理后:
b x λa0.
a a0 λa1.
c x λa2.
a1 a2
用我們熟悉到不能再熟悉的Haskell的Monad的手法來(lái)翻譯一下其實(shí)就是:
a0 <- b(x)
a1 <- a(a0)
a2 <- c(x)
return (a1(a2))
好了,至于上面這種形式(看起來(lái)很像SSA)是怎么被做成機(jī)器碼的,大家自己去看編譯原理吧。上面這么多廢話(huà)就是想表達(dá)一個(gè)結(jié)論:抽象并不意味著負(fù)擔(dān)。當(dāng)然,至于對(duì)程序員的智商上的要求,對(duì)某些人也是一種負(fù)擔(dān),這個(gè)我就沒(méi)辦法了,所以就不考慮他了。
===============廢話(huà)結(jié)束================
模板也是這類(lèi)抽象的一種。為什么我要把標(biāo)題寫(xiě)成“坑”,只是想跟前面統(tǒng)一一下而已,其實(shí)到了模板這么高級(jí)的抽象的時(shí)候,基本上已經(jīng)沒(méi)什么坑了。當(dāng)然C++的做法就另當(dāng)別論了,而且我想那些坑你們大概一輩子也碰不到的了。那我們先從簡(jiǎn)單的講起。
比模板更簡(jiǎn)單的東西自然就是泛型了。為什么叫他泛型?因?yàn)榉盒蛯?shí)際上就是一種復(fù)制代碼的方法,它本身是沒(méi)有推導(dǎo)能力的,所以肯定談不上什么模板了。但是在大多數(shù)情況下,泛型這么弱的抽象也已經(jīng)基本夠用了。跟泛型相關(guān)的手法大約有三個(gè)。
第一個(gè)就是定義一個(gè)返回一個(gè)類(lèi)的函數(shù)(在這里參數(shù)是T):
class Array<T>
{
public Array(int count);
public int Count{get;}
public T this[int index]{get; set;}
}
第二個(gè)就是,調(diào)用這個(gè)函數(shù),參數(shù)給他類(lèi)型,幫我們new一個(gè)類(lèi)的實(shí)例:
var xs = new Array<int>(10);
這其實(shí)有兩步。第一步是對(duì)函數(shù)調(diào)用Array<int>求值得到一個(gè)T,然后對(duì)new T(10)進(jìn)行求值獲得一個(gè)對(duì)象。只是剛好Array<int>的返回值也叫Array<int>,所以比較混淆視聽(tīng)。
事情到這里還沒(méi)完。上一篇文章中我們講到,寫(xiě)一個(gè)類(lèi)是要考慮很多contract的問(wèn)題的。所以Array<T>是個(gè)什么東西呢?他至少是一個(gè)IEnumerable<T>:
interface IEnumerable<out T>
{
// ...
}
class Array<T> : IEnumerable<T>
{
public Array(int count);
public int Count{get;}
public T this[int index]{get; set;}
}
于是有一天我們構(gòu)造了一個(gè)Array<Array<string>>的對(duì)象,然后要寫(xiě)一個(gè)函數(shù)來(lái)處理他。這個(gè)函數(shù)做的事情很簡(jiǎn)單,就是把這個(gè)二維的數(shù)組給平攤成一個(gè)一維的數(shù)組,里面所有的數(shù)組頭尾相接起來(lái)。于是根據(jù)上一篇文章的內(nèi)容,我們寫(xiě)一個(gè)接受class的函數(shù),也是要想很多contract的問(wèn)題的(面向?qū)ο缶褪锹闊┌。_@個(gè)函數(shù)需要的只是遍歷的功能,那我們完全沒(méi)有必要要求他必須是一個(gè)Array,于是我們的函數(shù)就這么寫(xiě):
IEnumerable<T> Flatten<T>(IEnumerable<IEnumerable<T>> xss)
{
foreach(var xs in xss)
foreach(var x in xs)
yield return x;
}
或者你也可以用高級(jí)一點(diǎn)的寫(xiě)法,反正是一樣的:
IEnumerable<T> Flatten<T>(IEnumerable<IEnumerable<T>> xss)
{
return xss.Aggregate(new T[], Enumerable.Concat);
}
有CPS變換就是好呀,沒(méi)有CPS變換的語(yǔ)言都寫(xiě)不出yield return和async await的。但是你們這些搞前端的人,特別是做nodejs的也是,都是教條主義的,覺(jué)得eval是evil,硬是把老趙的windjs(曾用名:jscex)給拒了。虧js還是一個(gè)特別適合寫(xiě)callback的語(yǔ)言呢,結(jié)果沒(méi)有$await,你們只能把一個(gè)好好的程序?qū)懗梢恢Щ鸺恕?/p>
那現(xiàn)在問(wèn)題來(lái)了。當(dāng)我們想把Array<Array<string>>傳給Flatten的時(shí)候,我們發(fā)現(xiàn)Flatten的參數(shù)需要的是IEnumerable<IEnumerable<string>>,究竟二維的數(shù)組能不能轉(zhuǎn)成二維的迭代器呢?
C++嘛,因?yàn)樗鼪](méi)有C#的協(xié)變和逆變的功能,所以是做不到的了。幸好我們這里用的是C# 4.0。那C#究竟是怎么做的呢?
其實(shí)從Array<Array<string>>到IEnumerable<IEnumerable<string>>需要兩步。第一步因?yàn)锳rray繼承自IEnumerable,所以類(lèi)型變成了IEnumerable<Array<string>>。第二部就是最重要的步驟了,因?yàn)镮Enumerable<out T>的T有一個(gè)out,這就說(shuō)明,IEnumerable里面所有的T都是用在函數(shù)的返回值上的,他只生產(chǎn)T,不會(huì)消耗T。所以一個(gè)IEnumerable<子類(lèi)型>就可以轉(zhuǎn)變成IEnumerable<父類(lèi)型>,因?yàn)樽宇?lèi)型總是可以轉(zhuǎn)變成父類(lèi)型的。因此最后IEnumerable<Array<string>>就變成了IEnumerable<IEnumerable<string>>了。
所以現(xiàn)在我們回過(guò)頭來(lái)看上面提到的泛型的三個(gè)手法
1、定義一個(gè)輸入類(lèi)型輸出類(lèi)型的函數(shù)(class Array<T>)
2、調(diào)用這個(gè)函數(shù)來(lái)得到你想要的類(lèi)型(new Array<int>())
3、協(xié)變和逆變((IEnumerable<IEnumerable<string>>)new Array<Array<string>>())
所以說(shuō)白了泛型就是一個(gè)對(duì)類(lèi)型進(jìn)行操作的東西。當(dāng)然它的內(nèi)涵遠(yuǎn)遠(yuǎn)沒(méi)有模板那么豐富,但是既然討論到了對(duì)類(lèi)型的操作,我覺(jué)得我要稍微普及一下一個(gè)類(lèi)型系統(tǒng)的常識(shí)——父類(lèi)型和子類(lèi)型。
父類(lèi)型和子類(lèi)型說(shuō)的是什么,如果我們有兩個(gè)類(lèi)型,一個(gè)T,一個(gè)U。如果一個(gè)U的值,總是可以被看成一個(gè)T的值的話(huà),那么我們就說(shuō)U是T的子類(lèi)型。我們也可以說(shuō),U是T的子集。這里我要說(shuō)一下為什么正方形是一個(gè)長(zhǎng)方形但是我們卻不能讓正方形繼承自長(zhǎng)方形呢?因?yàn)檫@個(gè)“是一個(gè)”只在只讀的時(shí)候成立。考慮了寫(xiě),正方形就不是子類(lèi)型了。
除了類(lèi)的繼承,協(xié)變逆變以外,還有另一種父類(lèi)型和子類(lèi)型的關(guān)系——那就是模板類(lèi)型了。舉個(gè)例子,我有一個(gè)函數(shù)是這么寫(xiě)的:T Do<T>(T t)。這是一個(gè)輸入T輸出T的模板函數(shù)。那我們有一天需要一個(gè)輸入int輸出int的函數(shù)怎么辦呢?Do<int>就好了。反過(guò)來(lái)行不行呢?不行。所以delegate T F<T>(T t)就是delegate int F(int t)的子類(lèi)型。
當(dāng)然,上面這個(gè)例子千萬(wàn)不能和函數(shù)的協(xié)變逆變混起來(lái)了。只有模板才能這么做,delegate string(string)肯定變不了delegate object(object)的。只有delegate string(object)可以通過(guò)協(xié)變+逆變同時(shí)作用變成delegate object(string)。因?yàn)閛bject和string是繼承的關(guān)系,不是模板的關(guān)系。
泛型到這里基本上就說(shuō)完了,輪到模板了。C#的泛型產(chǎn)生的類(lèi)可以是靜態(tài)類(lèi),其實(shí)C++就更可以了,而且C++還有偏特化這種必不可缺的東西。那偏特化有什么用呢?這跟上一篇文章將面向?qū)ο蟮臅r(shí)候一樣,其中的一個(gè)很大的用處就是拿來(lái)做contract。
interface和template(其實(shí)是concept mapping)拿來(lái)做contract的區(qū)別,在于你選擇一個(gè)具有contract的類(lèi)型的實(shí)現(xiàn)的時(shí)候,是在運(yùn)行時(shí)做的,還是在編譯時(shí)做的。其實(shí)有很多時(shí)候我們并不想,有時(shí)候也不能入侵式地給一個(gè)類(lèi)隨便添加一個(gè)新功能。我們知道,功能就是contract,contract要么是interface,要么是template。interface并不是總是可以加上去的,譬如說(shuō)對(duì)那些不能修改的類(lèi),就像string啊int什么的。而且我們也不會(huì)拿string和int做多態(tài)。這種時(shí)候我們就需要concept mapping了,然后靠類(lèi)型推導(dǎo)去選擇正確的實(shí)現(xiàn)。在C++里面,就是用template+偏特化來(lái)做了。
上面這段話(huà)說(shuō)得好像很抽象(一點(diǎn)都不抽象!),我們還是用一個(gè)生動(dòng)的例子來(lái)做吧。譬如說(shuō)我們要做序列化和反序列化。首先我們可以讓若干個(gè)類(lèi)型支持序列化的功能。其次,只要T支持序列化,那么vector<T>啊、list<T>什么的也將自動(dòng)支持序列化的功能。這是一個(gè)遞歸的描述,所以用template來(lái)做剛剛好。但是像vector和list這種不能修改的類(lèi),或者int這種原生的類(lèi)型,我們要怎么支持序列化呢?不能修改類(lèi),那只能寫(xiě)在類(lèi)的外面,變成一個(gè)函數(shù)了。那對(duì)于vector<T>來(lái)說(shuō),他怎么知道T的序列化函數(shù)是什么呢?相信熟悉模板的讀者肯定知道正確的答案是什么了。
首先我們要做一個(gè)空類(lèi)叫Serializable<T>,其次不斷地偏特化他們,覆蓋所有我們需要的類(lèi)型:
template<typename T>
struct Serializable
{
};
template<>
struct Serializable<int>
{
static int Read(istream& i);
static void Write(ostream& o, int v);
};
template<>
struct Serializable<wstring>
{
static wstring Read(istream& i);
static void Write(ostream& o, const wstring& v);
};
template<typename T>
struct Serializable<vector<T>>
{
static vector<T> Read(istream& i); // 我們已經(jīng)有右值引用構(gòu)造函數(shù)了,不怕!
static void Write(ostream& o, const vector<T>& v);
};
這里需要提到的一點(diǎn)是,當(dāng)我們使用Serializable<vector<T>>的時(shí)候,我們首先要保證T也是Serializable的。可惜的是C++并不能讓我們很好地表達(dá)這一點(diǎn),本來(lái)C++0x是有個(gè)concept mapping的標(biāo)準(zhǔn),不過(guò)后來(lái)被干掉了,我覺(jué)得永遠(yuǎn)都不會(huì)有這個(gè)東西了。但其實(shí)這并不是什么太大的事情,因?yàn)橹灰銓?xiě)錯(cuò)了,那總是會(huì)有編譯錯(cuò)誤的。
不過(guò)go語(yǔ)言在這一點(diǎn)上就不一樣了,go沒(méi)有模板什么像樣的代碼都寫(xiě)不出就不說(shuō)了,就算他有模板,然后同時(shí)具有Read和Write的類(lèi)型都實(shí)現(xiàn)了go的一個(gè)叫做Serializable的interface的話(huà),結(jié)果又如何呢?其實(shí)這相當(dāng)于把Serializable<T>::Read(i)和Serializable<T>::Write(o, v)都改成Read(i, &v)和Write(o, v)。這樣的問(wèn)題老趙已經(jīng)在他的博客講過(guò)了,萬(wàn)一Read和Write不是你寫(xiě)的,功能跟你要的不一樣,只是碰巧有了這兩個(gè)函數(shù)怎么辦,你還能救嗎?你已經(jīng)不能救了,因?yàn)槊侄加眠^(guò)了,你想顯式地實(shí)現(xiàn)一個(gè)interface的話(huà)go又沒(méi)這個(gè)功能,于是程序就到此傻逼了,Read和Write改名吧,祝你不是項(xiàng)目快寫(xiě)完了才發(fā)現(xiàn)這個(gè)問(wèn)題。
關(guān)于編譯錯(cuò)誤,我覺(jué)得有一個(gè)事情是很值得說(shuō)的。為什么熟悉Haskell都覺(jué)得Haskell的程序只要經(jīng)過(guò)了編譯基本上運(yùn)行就靠譜了?其實(shí)并不是Haskell的程序真的免去了調(diào)試的這一步,而是因?yàn)檫@門(mén)語(yǔ)言經(jīng)過(guò)了精心的設(shè)計(jì),把本來(lái)在運(yùn)行時(shí)才檢查的事情給轉(zhuǎn)到了編譯時(shí)。當(dāng)然這有一個(gè)不好的地方,就是我們用C語(yǔ)言來(lái)寫(xiě)一個(gè)程序的時(shí)候,雖然因?yàn)镃語(yǔ)言抽象能力太差被迫寫(xiě)的很糟糕,但是我們總可以運(yùn)行一點(diǎn)改一點(diǎn),最終讓他可以執(zhí)行。Haskell就不一樣了,只有能編譯和不能編譯兩種狀態(tài),你要不斷的修改程序,讓他可以編譯。一旦可以編譯,一般就好了。Haskell的這種特性需要淡定的程序員才能使用。
為什么呢,因?yàn)镠askell是沒(méi)有語(yǔ)句的,所以只要你修改了函數(shù)讓他做了不一樣的事情,那么函數(shù)的類(lèi)型就會(huì)發(fā)生變化。那么所有依賴(lài)到這個(gè)函數(shù)的函數(shù)的類(lèi)型也會(huì)發(fā)生變化。如果你改錯(cuò)了,那類(lèi)型檢查就會(huì)過(guò)不去,然后你的程序就不能編譯了。Erik Meijer菊苣說(shuō)得好,函數(shù)的類(lèi)型才是表達(dá)函數(shù)業(yè)務(wù)邏輯的地方。而之所以要函數(shù)體,那是因?yàn)榫幾g器不夠聰明,得讓你告訴他滿(mǎn)足這個(gè)類(lèi)型的最簡(jiǎn)單的解是什么。
所以如果我們?cè)贑++也采用這種寫(xiě)法的話(huà)——其實(shí)也就是把邏輯都交給template+偏特化,或者繼承+visitor來(lái)做,那么也會(huì)有一樣的效果,雖然并沒(méi)有Haskell那么嚴(yán)格。一旦你進(jìn)行了本質(zhì)上的邏輯的變動(dòng),那你的類(lèi)型一定會(huì)受到影響,那不滿(mǎn)足類(lèi)型要求的地方編譯器就會(huì)幫你找出來(lái)。所以,當(dāng)你看到一個(gè)因?yàn)檫@種情況而產(chǎn)生的編譯錯(cuò)誤的時(shí)候,心理要想:“好棒,編譯器又給我找出了一個(gè)錯(cuò)誤,避免我在運(yùn)行的時(shí)候才苦逼的調(diào)試它!”
當(dāng)然,模板的這些手法,可以很輕易地用在continuation passing style變換啊、combinator(很像設(shè)計(jì)模式但其實(shí)不是的東西)啊、async啊actor等各種強(qiáng)類(lèi)型的物體上面,不過(guò)這些東西我今天就不打算細(xì)說(shuō)了。當(dāng)我們?cè)谧鲱?lèi)似的事情的時(shí)候,我們要把類(lèi)型設(shè)計(jì)成能表達(dá)業(yè)務(wù)邏輯的那種形式,從而讓編譯器查更多的東西,把運(yùn)行時(shí)錯(cuò)誤盡量化簡(jiǎn)為編譯錯(cuò)誤。
當(dāng)然,C++的template比concept mapping還要更牛逼一個(gè)等級(jí)的,就是可以做type traits。如果是concept mapping是在對(duì)值通過(guò)類(lèi)型進(jìn)行分類(lèi)采用不同的計(jì)算方法的話(huà),那么type traits就是用來(lái)直接對(duì)類(lèi)型進(jìn)行計(jì)算的。那什么是對(duì)類(lèi)型進(jìn)行計(jì)算呢?我來(lái)舉個(gè)例子。
譬如說(shuō)我們要對(duì)一個(gè)vector進(jìn)行排序,但是這個(gè)時(shí)候我們不像std::sort一樣直接給出一個(gè)比較函數(shù),而是通過(guò)從T拿出一個(gè)U當(dāng)key的方法來(lái)做。譬如說(shuō)對(duì)Student類(lèi)排序,我們可以用他的學(xué)號(hào)、成績(jī)、姓名等等任何一個(gè)屬性來(lái)排序,這還是一個(gè)相當(dāng)有用的作法。當(dāng)然,我們總是可以寫(xiě)一個(gè)lambda表達(dá)式來(lái)做出一個(gè)用某個(gè)屬性來(lái)比較Student類(lèi)的函數(shù),然后讓std::sort來(lái)解決。很可惜的是,現(xiàn)在team的編譯器還不夠新,不支持C++11,怎么辦呢?于是我們決定擼一個(gè)自己的sort函數(shù)(雖然這種事情是不推薦的,但反正是個(gè)例子,就不糾結(jié)這個(gè)了):
template<typename T, typename U>
void Sort(vector<T>& ts, U(*key)(T))
{
for(int i=0;i<ts.size()-1;i++)
for(int j=ts.size()-1;j>i;j--)
if(key(ts[j-1]) > key(ts[j]))
swap(ts[j-1], ts[j]);
}
這段冒泡排序看起來(lái)沒(méi)什么問(wèn)題對(duì)吧,無(wú)論用什么語(yǔ)言最后寫(xiě)出來(lái)都肯定是這個(gè)樣子的。于是我們寫(xiě)了幾個(gè)單元測(cè)試,譬如說(shuō)用sin來(lái)對(duì)double排序啦,求個(gè)負(fù)數(shù)實(shí)現(xiàn)逆序啦等等,覺(jué)得都沒(méi)問(wèn)題。于是開(kāi)始投入實(shí)戰(zhàn)了!我們寫(xiě)了下面的代碼:
int GetScore(const Student& st)
{
return st.score;
}
vector<Student> students;
....
Sort(students, &GetScore); // error
為什么會(huì)錯(cuò)呢?因?yàn)镚etScore函數(shù)接受的不是Student而是const Student&!這下可麻煩了。我們有些函數(shù)接受的是T,有些函數(shù)接受的是const T&,難道要寫(xiě)兩個(gè)Sort嘛?當(dāng)然這種代價(jià)肯定是不能接受的。于是我們想,如果可以從U(*key)(T)的T推導(dǎo)出vector里面裝的是什么那該多好啊。反正無(wú)論函數(shù)接受的是string, string& const string, const string&,vector反正只能放string的。
這個(gè)時(shí)候就要祭出偉大的type traits了。怎么做呢?其實(shí)根上面的說(shuō)法一樣,我們把template看成是一個(gè)函數(shù),輸入是一個(gè)類(lèi)型,輸出再也不是值了,還是一個(gè)類(lèi)型就好了:
template<typename T>
struct RemoveCR
{
typedef T Result;
};
template<typename T>
struct RemoveCR<T&>
{
typedef typename RemoveCR<T>::Result Result;
};
template<typename T>
struct RemoveCR<const T>
{
typedef typename RemoveCR<T>::Result Result;
};
我們要怎么看這個(gè)過(guò)程呢?其實(shí)這是個(gè)pattern matching的過(guò)程,而pattern matching在一定程度上跟if-else其實(shí)是差不多的。所以我們看著一類(lèi)的東西,心里不要總是想著他是個(gè)模板,而是要想,RemoveCR是個(gè)函數(shù)。所以當(dāng)我們看見(jiàn)第一個(gè)RemoveCR的時(shí)候,我們心里浮現(xiàn)出來(lái)的景象大概是這個(gè)樣子的:
Type RemoveCR(Type T)
{
if(false);
....
else
return T;
}
好了,這個(gè)時(shí)候我們看見(jiàn)了第二個(gè)RemoveCR,那么這個(gè)RemoveCR函數(shù)又具體了一點(diǎn):
Type RemoveCR(Type T)
{
if(false);
else if(T is T0&)
return RemoveCR(T0);
....
else
return T;
}
后來(lái)我們看到了第三個(gè)RemoveCR,發(fā)現(xiàn)定義到此結(jié)束了,于是RemoveCR函數(shù)的實(shí)際的樣子就出來(lái)了:
Type RemoveCR(Type T)
{
if(false);
else if(T is T0&)
return RemoveCR(T0);
else if(T is const T0)
return RemoveCR(T0);
else
return T;
}
于是我們就可以做很多驗(yàn)證了,譬如說(shuō)RemoveCR<int>::Result的結(jié)果是int,RemoveCR<const int&>::Result的結(jié)果還是int。現(xiàn)在好了,我們可以修改我們的Sort函數(shù)了:
template<typename T, typename U>
void Sort(vector<typename RemoveCR<T>::Result>& ts, U(*key)(T))
{
....
}
無(wú)論你的排序函數(shù)接受的是Student還是const Student&,現(xiàn)在Sort函數(shù)都知道,你需要對(duì)vector<Student>進(jìn)行排序。于是任務(wù)就完成了!
別的語(yǔ)言里面沒(méi)有這種問(wèn)題,是因?yàn)橹挥蠧++才會(huì)把const、volatile和&這樣的東西用來(lái)修飾一個(gè)類(lèi)型而不是一個(gè)變量。這一點(diǎn)我第二篇文章已經(jīng)講過(guò)了,就不繼續(xù)啰嗦了。所以說(shuō)C++的設(shè)計(jì)雖然考慮得很周到,Bjarne Stroustrup菊苣也說(shuō)過(guò)他不喜歡像別的語(yǔ)言的作者一樣把自己的觀(guān)點(diǎn)強(qiáng)加給用戶(hù)。從這個(gè)出發(fā)點(diǎn)上來(lái)看,C++這一點(diǎn)相當(dāng)好。只要你肯學(xué)習(xí),又不會(huì)太蠢的話(huà),總是可以學(xué)會(huì)C++的正確使用方法,正常使用C++來(lái)寫(xiě)代碼的。但是,人真的蠢怎么辦呢?Bjarne Stroustrup你這樣歧視愚蠢的程序員是不對(duì)的,難道蠢就不能做程序員嗎,難道學(xué)了go陷進(jìn)去不能自拔的人就再也沒(méi)有機(jī)會(huì)學(xué)習(xí)C++了嗎!
關(guān)于type traits,haskell的type class(這東西就跟concept mapping一樣)其實(shí)也有一部分這樣的能力,可以幫你從type class的一部分類(lèi)型參數(shù)推導(dǎo)出另一部分的類(lèi)型參數(shù)。譬如說(shuō)你這么寫(xiě):
class MyClass a b c : a b => c
where ....
那么只要你實(shí)現(xiàn)了MyClass Int String Char,就不能實(shí)現(xiàn)MyClass Int String String了,因?yàn)閍 b => c這條規(guī)則已經(jīng)限制了,(Int String)只能出現(xiàn)一次,c要完全被a和b決定。所以擁有這個(gè)=>的haskell的type class也就可以有一部分type traits的功能了,雖然用法跟C++是截然不同的。
那C++的template除了泛型、concept和type traits之外,還有沒(méi)有別的功能呢?當(dāng)然有,我們不僅可以把類(lèi)型放進(jìn)模板的參數(shù)列表里面去,也可以把一個(gè)整數(shù)放進(jìn)去。這個(gè)時(shí)候我們就可以用Int<100>來(lái)表達(dá)100,用Pair<Int<100>, Pair<Int<200>, Pair<Int<300>, PairEnd>>>來(lái)代表數(shù)組[100, 200, 300],然后各種奇技淫巧就可以用來(lái)把模板寫(xiě)成一個(gè)不帶類(lèi)型的函數(shù)式語(yǔ)言了,在編譯期什么東西都可以算。這個(gè)事情展開(kāi)講就太復(fù)雜了,而且也沒(méi)什么用,你們感興趣的話(huà)去看《C++ Template Metaprogramming》就行了。
posted on 2013-05-12 00:31
陳梓瀚(vczh) 閱讀(10761)
評(píng)論(11) 編輯 收藏 引用 所屬分類(lèi):
啟示