一、為什麼要有函數模闆
二、什麼是函數模闆
三、函數模闆不是函數
3.1 隐式執行個體化
3.2 顯式執行個體化
四、函數模闆的使用
4.1 使用非類型形參
4.2 傳回值為auto
4.3 類成員函數模闆
4.4 函數模闆重載
4.5 函數模闆特化
五、變參函數模闆(模闆參數包)
5.1 遞歸
5.2 包擴充
5.3 參數包的轉發
六、其它
6.1 函數模闆 .vs. 模闆函數
6.2 [cv限定](https://zh.cppreference.com/w/cpp/language/cv)
七、參考
在泛型程式設計出現前,我們要實作一個swap函數得這樣寫:
但這個函數隻支援int型的變量交換,如果我們要做float, long, double, std::string等等類型的交換時,隻能不斷加入新的重載函數。這樣做不但代碼備援,容易出錯,還不易維護。C++函數模闆有效解決了這個問題。函數模闆擺脫了類型的限制,提供了通用的處理過程,極大提升了代碼的重用性。
cppreference中給出的定義是 “函數模闆定義一族函數”,怎麼了解呢?我們先來看一段簡單的代碼
swap支援多種類型的通用交換邏輯。它跟普通C++函數的差別在于其函數聲明(declaration)前面加了個template,這句話告訴編譯器,swap中(函數參數、傳回值、函數體中)出現類型T時,不要報錯,T是一個通用類型。
函數模闆的格式:
函數模闆在形式上分為兩部分:模闆、函數。在函數前面加上template<…>就成為函數模闆,是以對函數的各種修飾(inline、constexpr等)需要加在function-declaration上,而不是template前。如
parameter-list 是由英文逗号(,)分隔的清單,每項可以是下列之一:
序号
名稱
說明
1
非類型形參
已知的資料類型,如整數、指針等,C++11中有三種形式:
int N
int N = 1: 帶預設值,該值必須是一個常量或常量表達式
int …N: 模闆參數包(可變參數模闆)
2
類型形參
swap值用的形式,格式為:
typename name[ = default]
typename … name: 模闆參數包
3
模闆模闆形參
沒錯有兩個"模闆",這個比較複雜,有興趣的同學可以參考
cppreference之模闆形參與模闆實參
上面swap函數模闆,使用了類型形參。函數模闆就像是一種契約,任何滿足該契約的類型都可以做為模闆實參。而契約就是函數實作中,模闆實參需要支援的各種操作。上面swap中T需要滿足的契約為:支援拷貝構造和指派。
剛才我們提到函數模闆用來定義一族函數,而不是一個函數。C++是一種強類型的語言,在不知道T的具體類型前,無法确定swap需要占用的棧大小(參數棧,局部變量),同時也不知道函數體中T的各種操作如何實作,無法生成具體的函數。隻有當用具體類型去替換T時,才會生成具體函數,該過程叫做函數模闆的執行個體化。當在main函數中調用swap(a,b)時,編譯器推斷出此時T為int,然後編譯器會生成int版的swap函數供調用。是以相較普通函數,函數模闆多了生成具體函數這一步。如果我們隻是編寫了函數模闆,但不在任何地方使用它(也不顯式執行個體化),則編譯器不會為該函數模闆生成任何代碼。

仍以swap為例,我們在main中調用swap(a,b)時,就發生了隐式執行個體化。當函數模闆被調用,且在之前沒有顯式執行個體化時,即發生函數模闆的隐式執行個體化。如果模闆實參能從調用的語境中推導,則不需要提供。
在函數模闆定義後,我們可以通過顯式執行個體化的方式告訴編譯器生成指定實參的函數。顯式執行個體化聲明會阻止隐式執行個體化。
如果我們在顯式執行個體化時,隻指定部分模闆實參,則指定順序必須自左至右依次指定,不能越過前參模闆形參,直接指定後面的。
有些時候我們會碰到這樣一種情況,函數的傳回值類型取決于函數參數某種運算後的類型。對于這種情況可以采用auto關鍵字作為傳回值占位符。
decltype操作符用于查詢表達式的資料類型,也是C++11标準引入的新的運算符,其目的是解決泛型程式設計中有些類型由模闆參數決定,而難以表示的問題。為何要将傳回值後置呢?
函數模闆可以做為類的成員函數。
輸出:
需要注意的是:函數模闆不能用作虛函數。這是因為C++編譯器在解析類的時候就要确定虛函數表(vtable)的大小,如果允許一個虛函數是函數模闆,那麼就需要在解析這個類之前掃描所有的代碼,找出這個模闆成員函數的調用或顯式執行個體化操作,然後才能确定虛函數表的大小,而顯然這是不可行的。
函數模闆之間、普通函數和模闆函數之間可以重載。編譯器會根據調用時提供的函數參數,調用能夠處理這一類型的最佳比對版本。在比對度上,一般按照如下順序考慮:
順序
行為
最符合函數名和參數類型的普通函數
特殊模闆(具有非類型形參的模闆,即對T有類型限制)
普通模闆(對T沒有任何限制的)
4
通過類型轉換進行參數比對的重載函數
輸出
可以通過空模闆實參清單來限定編譯器隻比對函數模闆,比如main函數中的最後一條語句。
當函數模闆需要對某些類型進行特别處理,這稱為函數模闆的特化。當我們定義一個特化版本時,函數參數類型必須與一個先前聲明的模闆中對應的類型比對。函數模闆特化的本質是執行個體化一個模闆,而非重載它。是以,特化不影響編譯器函數比對。
上面的例子中針對const char *的特化,我們其實可以通過函數重載達到相同效果。是以對于函數模闆特化,目前公認的觀點是 沒什麼用,并且最好别用。Why Not Specialize Function Templates?
但函數模闆特化和重載在 重載決議 時有些細微的差别。這些差别中比較有用的一個是阻止某些隐式轉換。如當你隻有void foo(int)時,以浮點類型調用會發生隐式轉換,這可以通過特化來阻止:
雖然模闆配重載也可以達到同樣的效果,但特化版的意圖更加明确。
函數模闆及其特化版本應該聲明在同一個頭檔案中。 所有同名模闆的聲明應該放在前面,然後是這些模闆的特化版本。
這是C++11引入的新特性,用來表示任意數量的模闆形參。其文法樣式如下:
在模闆形參Args的左邊出現三個英文點号"…",表示Args是零個或多個類型的清單,是一個模闆參數包(template parameter pack)。正如其名稱一樣,編譯器會将Args所表示的類型清單打成一個包,将其當做一個特殊類型處理。相應的函數參數清單中也有一個函數參數包。與普通模闆函數一樣,編譯器從函數的實參推斷模闆參數類型,與此同時還會推斷包中參數的數量。
變參函數模闆主要用來處理既不知道要處理的實參的數目也不知道它們的類型時的場景。既然我們對實參數量以及類型都一無所知,那麼我們怎麼使用它呢?最常用的方法是遞歸。
通過遞歸來周遊所有的實參,這需要一點點的技巧,需要給出終止遞歸的條件,否則遞歸将無限進行。
該例子的技巧在于,函數2提供了const T &t參數,保證至少有一個參數,避免了與函數1在args為0時的沖突。需要注意的是,遞歸是指編譯器遞歸,不是運作過程時的遞歸調用。實際上編譯器為函數2生成了4個重載版本,并依次調用。下圖是在運作時的調用棧,可以看到共有5個重載版本的print函數,4個遞歸展開的函數2,外加函數1。遞歸最終結束在函數1處。
對于一個參數包,不管是模闆參數包還是函數參數包,我們對它能做的隻有兩件事:sizeof…()和包擴充。前面我們說過編譯器将參數包當作一個類型來處理,是以使用的時候需要将其展開,展開時我們需要提供用于每個元素的處理模式(pattern)。包擴充就是對參數包中的每一個元素應用模式,擷取得擴充後的清單。最簡單的包擴充方式就是我們在上節中看到的const Args &…和args…,該擴充是将其擴充為構成元素。C++11還支援更複雜的擴充模式,如:
運作程式将産生如下輸出:
擴充過程中模式(pattern)會獨立地應用于包中的每一個元素。同時pattern也可以接受多個參數,并非僅僅隻能接受參數包。
C++11中,我們可以同時使用變參函數模闆和std::forward機制來編寫函數,将實參原封不動地傳遞給其它函數。其中典型的應用是std::vector::emplace_back操作:
函數模闆重點在模闆。表示這是一個模闆,用來生成函數。
模闆函數重點在函數。表示的是由一個模闆生成而來的函數。
cv限定是指函數參數中有const、volatile或mutable限定。已指定、推導出或從預設模闆實參獲得所有模闆實參時,函數參數清單中每次模闆形參的使用都會被替換成對應的模闆實參。替換後:
所有數組類型和函數類型參數被調整成為指針
所有頂層cv限定符從函數參數被丢棄,如在普通函數聲明中。
頂層cv限定符的去除不影響參數類型的使用,因為它出現于函數中:
https://www.jianshu.com/p/949afb64be86