本節書摘來自異步社群出版社《c++ templates中文版》一書中的第2章,第2.1節,作者: 【美】david vandevoorde , 【德】nicolai m. josuttis,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
函數模闆提供了一種函數行為,該函數行為可以用多種不同的類型進行調用;也就是說,函數模闆代表一個函數家族。它的表示(即外形)看起來和普通的函數很相似,唯一的差別就是有些函數元素是未确定的:這些元素将在使用時被參數化。為了闡明這些概念,讓我們先來看一個簡單的例子。
2.1.1 定義模闆
下面就是一個傳回兩個值中最大者的函數模闆:
這個模闆定義指定了一個“傳回兩個值中最大者”的函數家族,這兩個值是通過函數參數a和b傳遞給該函數模闆的;而參數的類型還沒确定,用模闆參數t來代替。如例子中所示,模闆參數必須用如下形式的文法來聲明:
在我們這個例子裡,參數清單是typename t。可以看到:我們用小于号和大于号來組成參數清單外部的一對括号,并把它們稱作尖括号。關鍵字typename引入了所謂的類型參數t,到目前為止它是c++程式使用最廣泛的模闆參數;也可以用其他的一些模闆參數,我們将在後面介紹(見第4章)。
在上面程式中,類型參數是t。你可以使用任何辨別符作為類型參數的名稱,但使用t已經成為了一種慣例。事實上,類型參數t表示的是,調用者調用這個函數時所指定的任意類型。你可以使用任何類型(基本類型、類等)來執行個體化該類型參數,隻要所用類型提供模闆使用的操作就可以。例如,在這裡的例子中,類型t需要支援operator< ,因為a和b就是使用這個運算符來比較大小的。
鑒于曆史的原因,你可能還會使用class取代typename,來定義類型參數。在c++語言的演化過程中,關鍵字typename的出現相對較晚一些;在它之前,關鍵字class是引入類型參數的唯一方式,并一直作為有效方式保留下來。是以,模闆max()還可以有如下的等價定義:
從語義上講,這裡的class和typename是等價的。是以,即使在這裡使用了class,你也可以用任何類型(前提是該類型提供模闆使用的操作)來執行個體化模闆參數。然而,class的這種用法往往會給人誤導(這裡的class并不意味着隻有類才能被用來替代t,事實上基本類型也可以);是以對于引入類型參數的這種用法,你應該盡量使用typename。另外還應該注意,這種用法和類的類型聲明不同,也就是說,在聲明(引入)類型參數的時候,不能用關鍵字struct代替typename。
2.1.2 使用模闆
下面的程式展示了如何使用max()函數模闆:
在上面的程式裡,max()被調用了3次,調用實參每次都不相同:一次用兩個int,一個用兩個double,一次用兩個std::string。每一次都計算出兩個實參的最大值,而調用結果是産生如下的程式輸出:
可以看到:max()模闆每次調用的前面都有域限定符 :: ,這是為了确認我們調用的是全局名字空間中的max()。因為标準庫也有一個std::max()模闆,在某些情況下也可以被使用,是以有時還會産生二義性[1]。
通常而言,并不是把模闆編譯成一個可以處理任何類型的單一實體;而是對于執行個體化模闆參數的每種類型,都從模闆産生出一個不同的實體[2]。是以,針對3種類型中的每一種,max()都被編譯了一次。例如,max()的第一次調用:
使用了以int作為模闆參數t的函數模闆。是以,它具有調用如下代碼的語義:
這種用具體類型代替模闆參數的過程叫做執行個體化(instantiation)。它産生了一個模闆的執行個體。遺憾的是,在面向對象的程式設計中,執行個體和執行個體化這兩個概念通常會被用于不同的場合——但都是針對一個類的具體對象。然而,由于本書叙述的是關于模闆的内容,是以在未做特别指定的情況下,我們所說的執行個體指的是模闆的執行個體。
可以看到:隻要使用函數模闆,(編譯器)會自動地引發這樣一個執行個體化過程,是以程式員并不需要額外地請求模闆的執行個體化。
類似地,max()的其他調用也将為double和std::string執行個體化max模闆,就像具有如下單獨的聲明和實作一樣:
如果試圖基于一個不支援模闆内部所使用操作的類型執行個體化一個模闆,那麼将會導緻一個編譯期錯誤,例如:
于是,我們可以得出一個結論:模闆被編譯了兩次,分别發生在
1.執行個體化之前,先檢查模闆代碼本身,檢視文法是否正确;在這裡會發現錯誤的文法,如遺漏分号等。
2.在執行個體化期間,檢查模闆代碼,檢視是否所有的調用都有效。在這裡會發現無效的調用,如該執行個體化類型不支援某些函數調用等。
這給實際中的模闆處理帶來了一個很重要的問題:當使用函數模闆,并且引發模闆執行個體化的時候,編譯器(在某時刻)需要檢視模闆的定義。這就不同于普通函數中編譯和連結之間的差別,因為對于普通函數而言,隻要有該函數的聲明(即不需要定義),就可以順利通過編譯。我們将在第6章讨論這個問題的處理方法。在此,讓我們隻考慮最簡單的例子:通過使用内聯函數,隻在頭檔案内部實作每個模闆。