目錄
函數模板
類模板
變量模板
模板全特化
模板偏特化
模板顯式實例化解決文件分離問題
折疊表達式
模板的二階段編譯
待決名(dependent name)
SFINAE
概念與約束
函數模板
函數模板不是函數,只有實例化的函數模板,編譯器才能生成實際的函數定義,不過在很多時候,它看起來就像普通函數一樣。
示例:
下面是一個函數模板,返回兩個對象中較大的那個:
template<typename T>
T max(T a,T b){return a > b ? a : b;
}
這里其實對T有幾個要求,1:有>運算符,比如內置的int,double; 2:返回了一個T,即要求T是可以移動或復制的。 如果函數模板實參不滿足以上要求,則匹配不到此模板。
C++17 之前,類型 T 必須是可復制或移動才能傳遞參數。C++17 以后,即使復制構造函數和移動構造函數都無效,因為 C++17 強制的復制消除的存在,也可以傳遞臨時純右值。
使用模板
template<typename T>
T max(T a, T b) {return a > b ? a : b;
}int main(){int a{ 1 };int b{ 2 };max(a, b); // 函數模板 max 被推導為 max<int>max<double>(a, b); // 傳遞模板類型實參,函數模板 max 為 max<double>
}
模板函數可手動指定模板形參類型,也可以讓編譯器推導,即模板參數推導(template argument deduction),c++11支持函數模板參數推導,但是類模板參數推導要到c++17才支持。
對于編譯無法推導的場景,可手動指定,如:
max<double>(1, 1.2);
max<std::string>("luse"s, "樂");
但是 std::string 沒有辦法如此操作,編譯器會報:
<source>:31:4: error: call of overloaded 'max(std::string, std::string)' is ambiguous
31 | max(string("luse"), string("樂"));
| ~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:23:3: note: candidate: 'T max(const T&, const T&) [with T = std::__cxx11::basic_string<char>]'
23 | T max(const T& a, const T& b) {
| ^~~
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/bits/stl_algobase.h:257:5: note: candidate: 'constexpr const _Tp& std::max(const _Tp&, const _Tp&) [with _Tp = __cxx11::basic_string<char>]'
257 | max(const _Tp& __a, const _Tp& __b)
即函數有二義性,這是因為自己所編寫的max函數和標準庫的max函數沖突了,但是int double實例化max函數并不會。原因是string在std命名空間,標準庫的max函數也在std命名空間,雖然我們沒有使用?std::
,但是根據 C++ 的查找規則,(實參依賴查找)ADL,依然可以查找到。
那么我們如何解決呢?很簡單,進行有限定名字查找,即使用?::
?或?std::
?說明,你到底要調用 “全局作用域”的 max,還是 std 命名空間中的 max。
::max(string("luse"), std::string("樂"));
有默認實參的模板類型形參
就如同函數形參可以有默認值一樣,模板形參也可以有默認值。
template<typename T = int>
void f();f(); // 默認為 f<int>
f<double>(); // 顯式指明為 f<double>using namespace std::string_literals;template<typename T1,typename T2,typename RT = decltype(true ? T1{} : T2{}) >RT max(const T1& a, const T2& b) { // RT 是 std::stringreturn a > b ? a : b;
}int main(){auto ret = ::max("1", "2"s);std::cout << ret << '\n';
}
讓 max 函數模板接受兩個參數的時候不需要再是相同類型,那么這自然而然就會引入另一個問題了,如何確定返回類型?
typename RT = decltype(true ? T1{} : T2{})
這是一個三目運算符表達式。然后外面使用了 decltype 獲取這個表達式的類型,那么問題是,為什么是 true 呢?以及為什么需要 T1{},T2{} 這種形式?
1:我們為什么要設置為?true?
其實無所謂,設置 false 也行,true 還是 false 不會影響三目表達式的類型。這涉及到了一些復雜的規則,簡單的說就是三目表達式要求第二項和第三項之間能夠隱式轉換,然后整個表達式的類型會是?“公共”類型。
比如第二項是 int 第三項是 double,三目表達式當然會是 double。
using T = decltype(true ? 1 : 1.2);
using T2 = decltype(false ? 1 : 1.2);
2:為什么需要?T1{}
,T2{}
?這種形式?
沒有辦法,必須構造臨時對象來寫成這種形式,這里其實是不求值語境,我們只是為了寫出這樣一種形式,讓 decltype 獲取表達式的類型罷了。
模板的默認實參的和函數的默認實參大部分規則相同。
decltype(true ? T1{} : T2{})
?解決了。
事實上上面的寫法都十分的丑陋與麻煩,我們可以使用 auto 簡化這一切。
template<typename T,typename T2>
auto max(const T& a, const T2& b) -> decltype(true ? a : b){return a > b ? a : b;
}
這是 C++11 后置返回類型,它和我們之前用默認模板實參?RT
?的區別只是稍微好看了一點嗎?
不,它們的返回類型是不一樣的,如果函數模板的形參是類型相同?true ? a : b
?表達式的類型是?const T&
;如果是?max(1, 2)
?調用,那么也就是?const int&
;而前面的例子只是?T
?即?int
(前面都是用模板類型參數直接構造臨時對象,而不是有實際對象,自然如此,比如?T{}
)。
使用 C++20 簡寫函數模板,我們可以直接再簡化為:
decltype(auto) max(const auto& a, const auto& b) {return a > b ? a : b;
}
效果和上面使用后置返回類型的寫法完全一樣;C++14 引入了兩個特性:
-
返回類型推導(也就是函數可以直接寫 auto 或 decltype(auto) 做返回類型,而不是像 C++11 那樣,只是后置返回類型。
-
decltype(auto)?“如果返回類型沒有使用 decltype(auto),那么推導遵循模板實參推導的規則進行”。我們上面的?
max
?示例如果不使用 decltype(auto),按照模板實參的推導規則,是不會有引用和 cv 限定的,就只能推導出返回?T
?類型。即直接寫auto會丟棄CV限定符,但decltype(auto)按decltype的規則推導類型,會保留CV限定符。
非類型模板形參
既然有”類型模板形參“,自然有非類型的,顧名思義,也就是模板不接受類型,而是接受值或對象。
template<std::size_t N>
void f() { std::cout << N << '\n'; }f<100>();
非類型模板形參有眾多的規則和要求,目前,你簡單認為需要參數是“常量”即可。
非類型模板形參當然也可以有默認值:
template<std::size_t N = 100>
void f() { std::cout << N << '\n'; }f(); // 默認 f<100>
f<66>(); // 顯式指明 f<66>
重載函數模板?
函數模板與非模板函數可以重載。
這里會涉及到非常復雜的函數重載決議,即選擇到底調用哪個函數。
我們用一個簡單的示例展示一部分即可:
template<typename T>
void test(T) { std::puts("template"); }void test(int) { std::puts("int"); }test(1); // 匹配到test(int)
test(1.2); // 匹配到模板
test("1"); // 匹配到模板
- 通常優先選擇非模板的函數。
可變參數模板
和其他語言一樣,C++ 也是支持可變參數的,我們必須使用模板才能做到。
老式 C 語言的變長實參有眾多弊端,參見。
同樣的,它的規則同樣眾多繁瑣,我們不會說太多,以后會用到的,我們當前還是在入門階段。
我們提一個簡單的需求:
我需要一個函數 sum,支持 sum(1,2,3.5,x,n...) 即函數 sum 支持任意類型,任意個數的參數進行調用,你應該如何實現?
首先就要引入一個東西:形參包
本節以?C++14?標準進行講述。
模板形參包是接受零個或更多個模板實參(非類型、類型或模板)的模板形參。函數形參包是接受零個或更多個函數實參的函數形參。
template<typename...Args>
void sum(Args...args){}
這樣一個函數,就可以接受任意類型的任意個數的參數調用,我們先觀察一下它的語法和普通函數有什么不同。
模板中需要 typename 后跟三個點 Args,函數形參中需要用模板類型形參包后跟著三個點 再 args。
args 是函數形參包,Args 是類型形參包,它們的名字我們可以自定義。
args 里,就存儲了我們傳入的全部的參數,Args 中存儲了我們傳入的全部參數的類型。
那么問題來了,存儲很簡單,我們要如何把這些東西取出來使用呢?這就涉及到另一個知識:形參包展開。
void f(const char*, int, double) { puts("值"); }
void f(const char**, int*, double*) { puts("&"); }template<typename...Args>
void sum(Args...args){ // const char * args0, int args1, double args2f(args...); // 相當于 f(args0, args1, args2)f(&args...); // 相當于 f(&args0, &args1, &args2)
}int main() {sum("luse", 1, 1.2);
}