模板(template)是C++独有的特性,模板就是编译器生成特定类类型或函数的蓝图。生成特定类或函数的过程称为实例化。只需编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。
模板分:函数模板和类模板。
模板是标准库的基础,标准库算法是函数模板,标准库容器是类模板。
声明与定义
一个函数模板的基本结构如下:
// template: 模板关键字template开头。
// tempalte<...>: ...是模板参数列表,不能为空。里面是模板参数,分类型参数和非类型参数。
template<typename T, ...>
int compare(const T& v1, const T& v2) {
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
// T: 类型参数
// M: 非类型参数
template<typename T, unsigned int M>
int fuck(const T& v1, const T (&)[M]){
}
函数模板的声明、定义与函数类似。
// *******************************************************
// 声明函数模板compare
// *******************************************************
template <typename T> // 必须包含模板参数列表,可以省略名字(T)。
// 模板参数列表的类型与数目及顺序必须与模板定义时相同。
int compare(const T&, const T&); // 声明,无需函数体。
// *******************************************************
// 定义模板compare
// *******************************************************
template <typename T> // 定义时的模板参数列表。
int compare(const T&, const T&)
{
......
}
模板代码规范
// **********************************************************
// 一段不规范的模板代码
// **********************************************************
template<typename T>
int compare(T v1, T v2) { // 值传递,有如下弊端:
// 1、T类型必须支持拷贝构造
// 2、拷贝构造的代价可能更大,影响性能。
if(v1 < v2) return -1; // T类型必须支持 < 运算符
if(v1 > v2) return 1; // T类型必须支持 > 运算符
return 0;
}
// **********************************************************
// 一段较规范的模板代码
// **********************************************************
template<typename T>
int compare(const T& v1, const T& v2){ // 形参使用const引用。
if(less<T>(v1, v2)) return -1; // 使用标准库函数对象,最大化可移植性
if(less<T>(v2, v1)) return 1; // 只需支持less一个即可。
// less比<更好,用于指针时也没有问题。
return 0;
}
重要模板代码规范原则:
- 形参使用const引用
- 条件判断尽量只使用<运算符,less
更好。
实例化
在编译阶段,编译器会根据函数的实参推断出模板参数的值,然后实例化(instantiate)一个特定版本的函数,这些是模板的实例(instantiation)。
函数模板在使用时才生成代码(实例化)。
compare(1, 0); // 实例化出int compare(const int&, const int&)版本
compare(1.0, 2.0); // 实例化出int compare(const double&, const double&)版本
vector<int> vec1, vec2; //
compare(vec1, vec2); // 实例化出int compare(const vector<int>&, const vector<int>&)
显式实例化
模板在使用时实例化,这是被动的。我们也可以主动控制模板的实例化。这样可以避免在多个独立编译的文件中有相同的模板实例,造成严重额外开销。
就好像每个文件都独自定义变量,可以借鉴变量的extern跨文件访问特性。
// 声明模板实例,extern表示我们用到了外部的一个模板实例。
extern template declaration;
// 定义模板实例(显式实例化)
template declaration; // declaration是一个实例化的模板,不是模板。
// **********************************************************************************
// ******** 例 子
// **********************************************************************************
// 声明Blob<string>模板实例
// 该实例在其他文件被定义了。编译器会链接定义该实例的文件。
extern template class Blob<string>;
// 定义函数模板实例
// 这里是已经产生代码了,相当与使用了一次compare(1, 2)
template int compare(const int&, const int&);
// 定义类模板实例
template class Blob<string>; // 这里会实例化类内部的所有成员(包括成员模板函数)
特例化
函数模板的特例化必须为所有模板参数提供实参。
// 原模板
template <typename T> int compare (const T&, const T&);
// 模板的特例化
template<> // 必须是template<>,表示为所有模板参数都提供了实参。。
int compare(......) // 注意,参数类型必须与原模板对应。
{ // 原模板是常量的引用
}
模板和模板特例化应该声明在同一个头文件中。所有同名模板的声明放在特例化版本的前面。
特例化是模板的一个实例,并不是模板的重载,并不影响函数匹配。
实例化:模板生成代码的过程,并不能定义模板内部代码逻辑。
特例化:针对模板的某一特定类型(提供所有模板参数的实参),重新定义模板内部逻辑。
模板编译
编译器遇到模板定义时,不会生成代码。编译器遇到模板使用时(实例化),才会生成代码。为了实例化代码,编译器需要当场就知道模板函数的定义(而不是链接的时候知道),因此模板函数的声明与定义会放在一个头文件中。
编译模板时,会三个阶段产生错误:
- 编译模板本身,只能发现一些明显错误,如忘记分号、变量名拼错等。
- 编译模板使用处时,只能发现一些基础错误,如参数数目匹配、类型是否匹配等。
- 编译模板的实例时,编译模板生成的最终代码,这和普通的编译就一样了。
模板参数
C++_模板参数模板重载
函数模板可以被另一个模板函数或非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数 。函数匹配规则:
- 候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。
- 如果有多个函数提供同样好的匹配:
template
string s(“hi”); fuck(s); // 调用第一个版本 fuck(&s); // 第一个版本;fuck(const string&) // 第二个版本:fuck(string) // 第二个版本是精确匹配,所以选择第二个版本。
const string sp = &s; fuck(sp); // 第一个版本:fuck(const string&)精确匹配 // 第二个版本:fuck(const string)精确匹配 // 好像有二义性,其实没有。 // fuck(const T&)比fuck(T)更通用。 // fuck(T*)更特例化,所以选择了后者。 // fuck(const T&)是可以接受任何类型参数的。
<a name="iObx3"></a>
## 非模板与模板间重载
如果匹配程度一样,编译器会优先选择非模板。
```cpp
template <typename T>
void fuck(const T &t ) { }
template <typename T>
void fuck(T *p) { }
void fuck(const string &s){ }
string s("hi");
fuck(s);
// 第一个版本:fuck<string>(const string&),精确匹配。
// 第三个版本:fuck(const string&),普通非模板函数,精确匹配。
// 匹配同样好的情况下,编译器优先选择非模板。
fuck("hi world!");
// fuck(const T&), T被绑定到char[10],精确匹配
// fuck (T*), T被绑定到const char,精确匹配
// fuck(const string&),要求从const char*到string的类型转换。不是精确匹配
// 模板匹配同样好的情况下,优先选择更特例的模板,也就是第二个模板。
注意
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的函数。
// ********************************************************
// test.h头文件
// ********************************************************
template <typename T>
void fuck(const T &t ) { }
template <typename T>
void fuck(T *p) { }
void fuck(const string &s){ }
void fuck(char *p) { }
void fuck(const char *p){ }
// ********************************************************
// fuck.cpp源文件
// ********************************************************
// 先声明所有的重载版本
template <typename T> void fuck (const T &t); // 重载版本1
template <typename T> void fuck (T *p );
void fuck(const string&); // 重载版本2
void fuck(char *p) {
// 如果重载版本1没有声明,则将调用重载版本2的T为string的实例化版本。
// 返回语句将调用debug_rep(const T&)的T实例化为string的版本
fuck(string(p));
}
模板实参推断
template argument deduction,在函数调用的时候来确定模板实参。类模板没有实参推断,类模板在使用时必须显式确定模板实参。
类型转换
在模板函数被调用时,若形参的类型是模板类型参数,编译器一般不会自行类型转换,而是生成新的模板实例,除了以下情况:
- 形参、实参中,顶层const都会被忽略。
- 数组、函数会自动转换成指针。
- 注意,不会进行一般函数调用中的算术类型转换。
若形参是普通类型定义的参数, 或者在函数调用处显式指定了模板类型参数,则进行普通函数的形参类型转换。
template<typename T> T fobj(T, T); // 值传递版
template<typename T> T fref(const T&, constT &); // 引用版,是底层const,不会被忽略。
string s1("aa");
const string s2("aaa"); // 是顶层const,可以被忽略。
fobj(s1, s2); // 调用fobj(string, string),s2的顶层const被忽略
fref(s1, s2); // 调用fref(const string&, const string&)
//将s1转换为const是允许的
int a[10], b[42];
fobj(a, b); // 调用f(int*,int*),自动转换成指针。
fref(a, b); // 错误:数组类型不匹配,数组大小也是数组类型的一部分。
long lng;
fobj<int>(lng, 1); // 正确,匹配fobj(int, int), long转换成int,因为显式指定了模板类型参数。
// 类型参数不同,可以增加兼容性。
template<typename A, typename B>
int compare(const A& v1, const B& v2){
if (v1 < v2 ) return -1;
if (v2 < v1 ) return l ;
return 0;
}
compare(1.0, 2); // compare(double, int)
// 如果类型参数一样,这里就会报错了。
函数模板显式实参
注意,不是默认实参,而是使用的地方显式指定模板类型参数的值。
有些情况,编译器无法自行推断类型,比如函数返回值类型,且类型形参列表类型都不相同,即默认类型参数并没有指定函数返回值类型,因为和形参不同,在使用处并没有类似“实参”的东西来让编译器对返回值做出推断,这时我们必须在每个函数使用处显式指定返回值类型的模板类型参数,见如下代码:
template<typename T1, typename T2, typename T3>
T1 sum(T2, T3){
......
}
int main(){
sum(1, 2); // 错误,编译器无法推断T1的类型。
// T2: int
// T3: int
sum<int>(1, 2); // 正确,显式指定了返回值类型的模板类型参数T1
// T1: int
// T2: int
// T3: int
return 0;
}
以下是一种糟糕的模板设计:
// 将返回值类型的模板实参放在最后面,这会使模板的使用变得非常麻烦
// 每次都得显式指定三个模板参数,因为T1是肯定要指定的,那前面的都必须要指定
// 而T2、T3是可以根据函数调用处的实参来推断出来的,无需显式指定。
template<typename T2, typename T3, typename T1>
T1 sum(T2, T3)
{
......
}
int main()
{
sum<long>(1, 2); // 错误,编译器无法推断T1的值
// T2: long
// T3: int
sum<int, int>(1, 2); // 错误,编译器无法推断T1的值
// T2: int
// T3: int
sum<int, int, int>(1, 2); // 正确
return 0;
}
尾置返回类型
我们可以让用户显式指定模板参数来确定函数模板的返回值类型,但是这有时是非必要的,用户每次都要思考返回的到底是什么类型,这很麻烦。我们可以用位置返回类型:
// 函数模板的尾置返回类型和普通函数差不多
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg} // 返回类型和*beg一样,是元素的左值引用。
{
return *beg; // 返回序列中一个元素的引用
}
标准类型转换模板
上面的decltype并不能对付通用的情况,比如上面这个函数要返回元素的值,就搞不定了。我们可以利用标准库的类型转换模板,他们定义在type_traits头文件中。
// 函数模板的尾置返回类型和普通函数差不多
template<typename It>
auto fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
// remove_reference前面的typename是告诉编译器,type是类型,不是静态变量。
{
return *beg; // 返回序列中一个元素的拷贝。
}
标准库类型转换模板 | ||
---|---|---|
Mod |
若 T 为 | 则Mod |
remove_reference 去除引用 |
X&或 X&& | X |
否则 | T | |
add_const 返回const类型 |
X&、const X 或函数 |
T |
否则 | const T | |
add_lvalue_reference 返回左值引用类型 |
X& | T |
X&& | X& | |
否则 | T& | |
add_rvalue_reference 返回右值引用类型 |
X&或 X&& | T |
否则 | T&& | |
remove_pointer |
X* | X |
否则 | T | |
add_pointer | X&或 X&& | X* |
否则 | T* | |
make_signed | unsigned X | X |
否则 | T | |
remove_extent | X[n] | X |
否则 | T | |
remove_all_extents | X[n1][n2]…… | X |
否则 | T | |
每个类型转换模板的工作方式都与remove_reference类似。 每个模板都有一个名为type的public 成员,表示一个类型 remove_reference |
形参是模板函数地址
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
template <typename T> int compare(const T&, const T&);
// pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
// func的重载版本:每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 错误:使用compare的哪个实例?不知道是用string的还是int的。
func(compare<int>); // 正确:显式指出实例化哪个compare版本
形参是左值引用
则实参必须是一个左值。
template<typename T>
void f1( T& t) // 当形参是T&形式时,实参的类型就是T的类型。实参必须是左值。
{
......
}
int i = 1;
const int ci = 1;
f1(i);
// 实参类型是int,所以T是int。
// 对应模板实例是:f1<int>(int&)
f1(ci);
// 实参类型是const int,所以T是const int。
// 对应模板实例是:f1<const int>(const int&)
f1(5); // 错误,实参必须是左值。
/**************************************************************/
template<typename T>
void f2(const T&) // 当形式是const T&常量引用形式时候,实参中的const是无关的。
{
......
}
// 在每个调用中,f2的函数参数都被推断为const int&,因此T都是int
f2(i); // T是int,对应模板实例是f2<int>(const int&)
f2(ci); // T是int,对应模板实例是f2<int>(const int&)
f2(5); // T是int,对应模板实例是f2<int>(const int&)
形参是右值引用
参考文章:https://www.yuque.com/tvvhealth/cs/fpep24
形参是右值,则实参可以是任意类型,包括左值引用,模板类型参数推导较为复杂:
首先,推导模板类型参数T的值:
- 若实参是A类型的左值,则T推导为A&
- 若实参是const A类型特殊左值,则T推导为const A&
- 若实参是A类型的右值,则T推导为A&&
然后,是引用折叠规则:
- A& & 变成 A&
- A& && 变成 A&
- A&& & 变成 A&
- A&& && 变成 A&&
template<typename T>
void f3( T&& val)
{
......
}
f3(42);
// 1、实参是右值,T推导为T&&
// 2、代入模板中,T&& && 折叠为T&&
// 对应的模板实例化是:f3<T&&>(T&&)
int i;
f3(i);
// 1、实参是左值,T推导为T&
// 2、代入模板中,T& && 折叠为T&
// 对应的模板实例化是:f3<T&>(T&)
const int ci = 1;
f3(ci);
// 1、实参是特殊左值,T推导为const int&
// 2、const int& && 折叠为const int&
// 对应的模板实例是:f3<const int&>(const int&)
std::move
更多细节参考:https://www.yuque.com/tvvhealth/cs/fpep24
根据上面的情况,如果形参类型是模板参数的右值引用,则模板参数类型的不能确定,可能是普通类型、可能是右值引用类型、可能是左值引用类型。那这样写代码就麻烦了。比如:
template <typename T>
void f3(T&& val)
{
// T可能是int、int&、int&&,那下面的逻辑行为就完全不同,这可咋搞。
T t = val; // 拷贝还是绑定一个引用?
t = fen(t); // 赋值只改变t还是既改变t又改变val?
if (val == t) // 若T是引用类型,则一直为true
{
......
}
}
std::move是形参为右值引用的模板的很好的例子。看看它是如何处理的?std::move是标准库函数它返回实参的右值引用。
// ********************************************************
// std::move的源码实现
// ********************************************************
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
// 无论实参是什么类型,typename remove_reference<T>::type&&一定是数据类型的右值引用。
// 我们可以假设实参分贝是左值类型、右值类型,const int类似的左值类型分别推导一下。
return static_cast<typename remove_reference<T>::type&&>(t);
}
// ********************************************************
// 根据实参类型,推断std::move的结果是否正确。
// ********************************************************
string s1("hi!"), s2;
s2 = std::move(string("bye!"));
// 实参是临时变量string("bye!"),是右值类型,T推断为T&&
// 代入std::move模板得到std::move<T&&>(T&& &&),引用折叠为:move<T&&>(T&&)
// 调用的模板实例为:string&& move(string&&)
// typename remove_reference<string&&>::type = string
// move的返回值类型为string&&,因此触发移动赋值运算
s2 = std::move(s1);
// 实参是左值类型,T推断为T&
// 代入std::move模板:std::move<T&>(T& &&),引用折叠为std::move<T&>(T&)
// 调用的模板实例为:string&& move(string&)
// typename remove_reference<string&>::type = string
// move的返回值类型为string&&,因此触发移动赋值运算
std::forward
更多细节参考:https://www.yuque.com/tvvhealth/cs/fpep24
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数 。 在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。标准库函数forward能保持原实参的类型,定义在头文件utility中。看下面例子,体会std::forward的效果。
void fuck( int &&i )
{
// i虽然是右值引用,但并不是右值而是左值,因为i有一个名字。
// 规则:有名字的右值引用是左值,没有名字的右值引用才是右值。
you(i); // 编译错误,i是左值,you的形参是右值引用,无法绑定到左值实参上。
you(std::forward<int&&>(i)); // 正确,forward返回i的右值引用int&&
}
void you( int &&i )
{
}