说明
本文来自《Effective Modern C++》,自己做了个学习笔记,一是加深印象,二是方便后期自己查阅,再来也希望帮助到有需要的同学。泛型编程是c++重中之重,模板是c++泛型编程的一大块,理解模板的类型推导才能更好的掌握c++模板。
模板形式
template<typename T>
void f(ParamType param);
在编译期,编译器会通过函数的实参分别推导T和ParamType的类型,ParamType相对于T,会额外加一些修饰词,如const或引用符号等限定词。
demo
- 模板函数声明
template<typename T>
void f(const T& param);
- 调用函数
int x = 0;
f(x);
这里T被编译器推导为int,而ParamType被推导为 const int&。如果我们没有深入的了解,可能就会认为实参类型是什么,T就会被推导为是什么类型,其实T的类型推导还要依赖ParamType。
三种情况
接下来我们看一下类型推导的三种情况:
情况 1:ParamType是指针或者引用,而不是万能引用
这是最简单的情况,注意两点即可:
- 如果实参是引用类型,则先忽略引用部分。
- 之后,根据实参的类型和ParamType的修饰词来决定T的类型。
示例 1:
函数模板
template<typename T>
void f(T& param);
变量
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是const int
const int& rx = x; // rx 的类型是const int 的引用
各次调用中,对param和T的推导结果如下:
f(x); // T 的类型是 int, param 的类型是 int&
f(cx); // T 的类型是 const int, param 的类型是 const int&
f(rx); // T 的类型是 const int,param 的类型是 const int&
解释:第一个是我们想当然的结果,很好理解。第二个和第三个调用中,由于函数形参加了引用修饰(T&),所以参数传递的时候,要保留实参的属性,也就是要保持实参对象的不可修改的属性。这也是为什么向持有 T& 类型的模板传入const对象是安全的:该对象的常量性(constness)会成为 T 的类型推导结果的组成部分。
接下来我们将函数形参类型加上const 关键字,看下稍微的区别:因为形参已经被const 修饰,所以推导结果 T 也就不需要const了。
示例 2:
函数模板
template<typename T>
void f(const T& param);
变量
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是const int
const int& rx = x; // rx 的类型是const int 的引用
各次调用中,对param和T的推导结果如下:
f(x); // T 的类型是 int, param 的类型是 int&
f(cx); // T 的类型是 int, param 的类型是 const int&
f(rx); // T 的类型是 int,param 的类型是 const int&
指针和引用没有区别
示例 3:
函数模板
template<typename T>
void f(T* param);
变量
int x = 27; // x 的类型是 int
const int *px = &x; // px 是指向x的指针,类型为 const int *
调用
f(&x); // T 的类型是 int, param 的类型是 int *
f(px); // T 的类型是 const int, param 的类型是 const int *
情况 2:ParamType 是万能引用
模板函数形参声明为 T&&(也就是右值引用语法)。两种不同情况:
- 如果实参是做是左值,T 和 Paramtype 都会被推导为左值引用。这里与我们所想大不一样:首先,这是在模板类型推导中,T被推导为引用类型行的唯一情况。其次,尽管声明时使用的是有值引用语法,但是推导结果却是左值引用。
- 如果实参是右值,那和情况1规则一样
例子
函数模板
template<typename T>
void f(T&& param); // param 现在是万能引用
变量
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是const int
const int& rx = x; // rx 的类型是const int 的引用
调用
f(x); // x 是左值,所以T 的类型是 int &, param 的类型也是 int &
f(cx); // cx 是左值,所以T 的类型是 const int &, param 的类型也是 const int &
f(rx); // rx是左值,所以T 的类型是 const int &, param 的类型也是 const int &
f(27); // 27 是左值,所以T 的类型是 int, param 的类型也是 int &&(右值引用)
关键在于,万能引用形参类型的推导会依据实参是右值还是左值。这里传左值时,大家可能不理解为什么T也被推导为引用类型,其实只要简单理解成T&&是一种特殊关键字就行了,此时它不是右值引用,二是万能引用。
情况 3: ParamType 既非指针也非引用
这时指的是值传递:
- 实参的引用还是忽略。
- const、volatile也要忽略。
例子 1
函数模板
template<typename T>
void f(T param); // param 现在是值传递
变量
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是const int
const int& rx = x; // rx 的类型是const int 的引用
调用
f(x); // T 和 param的类型都是 int
f(cx); // T 和 param的类型都是 int
f(rx); // T 和 param的类型都是 int
这里比较好理解,因为值传递进行实例化新的副本。所以实参原有的属性不应该影响副本。
这里有有个稍微不好理解的例子 const char * 类型,当然char也可以是int 或者其他。
例子 2
函数模板
template<typename T>
void f(T param); // param 现在是值传递
变量
const char * ptr = "ABC"; // ptr 指向一个常量对象
调用
f(ptr); // T 和 param 都是const char *
解释:因为值传递针对的是实参与形参之前的关系,我们不希望改变其他变量也就是 ptr 所指向的常量。
难点 :数组参数
为什么说这块难一点,因为我们都只到c/c++数组在传参时都会退化为指针。但是下面的例子则不然。
例子 1 :值传递
函数模板
template<typename T>
void f(T param); // param 现在是值传递
变量
int arr[7] = {}; // arr 长度为7的int类型数组
调用
f(arr); // T 和 param 都是 int *
这里例子好理解
例子 2 :引用传递
函数模板
template<typename T>
void f(T& param); // param 现在是值传递
变量
int arr[5] = {}; // arr 长度为7的int类型数组
调用
f(arr); // T 是实际数组类型 int [5], param 被推导为长度为7的int类型数组引用(也就是 int (&)[7] )
// sizeof(arr) = 20;
是不是很有意思。
网友评论