1、通過一個簡單的例子來理解模板的用途:

模板為不同類型的數據生成操作相同或相似的函數。
弱語言如Python,可以使用一種函數來應對各種類型,但是C++就不得不為不同的類型編寫相似的函數。模板的作用就是把這一步驟交給編譯器去執行,讓這些函數在編譯器生成。
2、模板參數的自動推導
原則:凡是可以推導出來的模板參數“值”就無需在模板實參列表中寫明。
規則一:
編譯器值根據函數調用時給出的實參列表來推導模板參數值,與函數參數類型無關的模板參數無法推導
規則二:與函數返回值相關的模板參數其值也無法推導
規則三:
所有可以推導模板參數必須是連續位於模板參數列表尾部,中間不能有不可退到的模板參數。
舉例:


test1~test3的分析過程如下:
第一,sv2是返回值,我們不能通過返回值的類型推導模板參數的值,所以T2無法推導出來;
第二,T0使用的地方是函數內部的一個變量,也不是函數的參數,所以無法通過sv0的類型推導出T0的值
第三,以test1為例,func的三個參數分別是1,2,3整型,所以可以推斷出T1、T3、T4是int。因為T0和T2無法推導出,所以必須在func<>中明確給出。而由於要根據聲明的順序給出,不能跳過T1,所以func<>中的三個類型分別是T0、T1、T2的類型。
3、模板參數的默認值
形如:

有兩個具有默認類型,其它三個可以從函數參數類型推導,所以不需要尖括號:

4、模板參數的靜態變量
這一講用來說明,如果模板參數的值是相同的,那么模板函數實例就只生成一個,不會重復生成。

test1和test2中的static變量進行了遞減,說明test1和test2中的func是同一個。這也就說明,
func的實例是依具體的參數而決定的,如果模板參數值一樣,編譯器就不會重復生成函數實例。
我們逆向也可以知道,這里的兩個函數是相同的:

用IDA加載進符號表之后,會更清晰一點:

5、
問題一:模板函數應該如何在頭文件里聲明?
如果
只是在頭文件里添加一個函數模板的聲明:

這樣編譯工程是肯定能編譯通過的。因為,編譯器在編譯test.h和test.cpp文件時,只是讀到了func0函數模板的實現,並沒有讀到任何需要生成函數模板的實例的語句,所以不會生成任何func0函數實例。

但是如果在main.cpp中添加了func0,那么此時就要生成一個func0的具體實例了,但test.h文件中只有一個func0函數模板的聲明,編譯器並沒有生成實際的函數實例,只好在mian函數中的func0處預留一個鏈接調用,等待在鏈接過程中找到函數實現。因為你這里調用時,實際上是調用一個func0<int>(0); 但是test.cpp里並沒有這個函數的實力。就會報錯(這種錯誤很隱晦,很難排查):

所以,
我們的解決辦法是明確地在頭文件中聲明具體的實例。編譯器就會生成這個函數模板實例了:

問題二:
但是,如果你想再生成func0(float)、func0(char)那么就得在頭文件中添加兩個實例的聲明。但這貌似違背了最初使用模板的目的。
解決的辦法就是把模板的實現也包含到頭文件中,這樣main函數把test.h包含進來了,編譯器在編譯時就知道main函數要用到一個func0<int>實例了,就可以編譯出func0<int>實例了。//如果你把代碼放到cpp里,那就是等到連接時去做事兒,如果放到h文件里,那就是在編譯時去做事兒。
問題三:
產生重復模板實例問題。但這個問題會由連接器解決。
比如,新增加caller.obj

原有的main.obj中也有

但我們會發現main和caller.obj最終調用的都是caller中的func0:

就是因為連接器把caller0和main中的func0合並成了一個,
合並規則是函數名、模板實參列表以及參數列表相同。