模板的型别推导是现代C++最广泛应用的特性之一,下面的一小段伪代码可以简单明了的说明函数模板使用场景。函数模板定义如下:
template<Class T>
void f(ParamType param);
而一次模板函数调用如下:
f(expr);
在编译期,编译器将会通过expr推导出T及ParamType的型别,这两个往往不一样。因为ParamType常会包含一些修饰词,例如const或者引用之类的。
例如,若模板函数声明如下:
template<class T>
void f(const T& param);
而调用语句为:
int x = 0; // x类型为int
f(x); // T类型为int,ParamType为const int&
此时,ParamType被推导为const int&,T被推导为int,这里需要说明一下,T的型别不仅仅依赖于expr的型别,还依赖于ParamType的形式,具体可以分为以下三种情况进行分析:
情况1:ParamType是一个指针或引用,但不是万能指针或引用
此时,型别推导会进行如下操作:
- 若expr是具有引用型别,则先将引用部分忽略
- 然后对expr的型别和ParamType进行型别匹配,进而决定T的型别。
结合以上两句并参考以下代码便可以理解:
template<class 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& 类型
//在各个调用时,param及T的推导如下:
f(x); //T是int,param 为 int&
f(cx); //T是const int,param为 const int&
f(rx); //T是const int,param为 const int&
template<class T>
void f(const T& param); //此时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为 const int&
f(cx); // T是int,param为 const int&
f(rx); // T是int,param为 const int&
template<class T>
void f(T* param); //此时param是指针
int x = 27;
const int* px = &x;
//在各个调用时,param及T的推导如下:
f(&x); // T是int,param 为 int*
f(px); // T是const int,param为 const int*
情况2:ParamType是一个万能引用
万能引用的声明型别写作T&&,其会根据实参类型型别,进行不同的处理:
- 如果expr是左值,T和ParamType都会被推导为左值引用,这是T被推导为引用的唯一情况
- 如果expr是右值,则按照常规情况进行处理(情况一)
结合以上两句并参考以下代码便可以理解:
template<class 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& 类型
在各个调用时,param及T的推导如下:
f(x); //x是左值,所以T是int&,param 也为 int&
f(cx); //cx是左值,所以T是const int&,param也为 const int&
f(rx); //x是左值,所以T是const int&,param为 const int&
f(27); //x是右值,所以T是int,param为int&&
关于万能引用,可以参考文章:《Effective Modern C++》学习笔记之条款二十四:区分万能引用和右值引用_木木的博客的博客-CSDN博客
情况3:Paramtype即非指针也非引用,即按值传递
如果是按值传递,则param相当于一个副本。
- 若expr具有引用型别,则先忽略其引用部分
- 若expr是个const、volatile对象,则也忽略其const、volatile关键字
结合以上两句并参考以下代码便可以理解:
template<class 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& 类型
在各个调用时,param及T的推导如下:
f(x); //T是int,param 也为 int
f(cx); //T是int,param 也为 int
f(rx); //T是int,param 也为 int
这里需要说明一下,虽然cx或者rx是const类型,而param仍然不具有const属性,这是合理的,因为此时param是rx或cx的一个副本,所以即使对param做修改,并不会影响到rx或cx的值,满足其const属性要求。
另外,若expr是一个指涉到const对象的const指针,且expr按值传递给param:
template<class T>
void f(T param);
const char* const ptr = "this is test";
f(ptr); //此时T和param均会被推导为const char*
这里星号右侧的const将ptr声明为const,表示其不可以修改其指涉的内存地址,星号左侧的const将ptr指涉的对象修饰为const,即对象内容不可更改。当将ptr传递给f()时,在型别推导中,ptr指涉对象的常量性会被保留,但其自身的常量性会被忽略,即其星号右侧的const属性将被忽略,所以,此时param被推导为const char*。
指针退化:
除了以上三种情况外,还有一种情况,就是指针退化的情况,包括数组退化为数组指针,函数退化为函数指针。
当我们把数组传入到某个数组指针形参时,是可以可以编译通过的,因为数组在传递时,会被退化为数组指针。
但是如果此时我们把ParamType声明为一个引用,再向其传入一个数组,此时数组将不会发生退化,即形参就是一个数组的引用,例如:
template<class T>
void f(T& param);
const char name[13] = “123456789”;
f(name); // T被推导为const char[10], param将会被推导为const char(&)[10]
有意思的是,我们可以利用声明数组引用这一能力创造出一个模板,用以推导数组中的元素个数:
template <class T,std::size_t N>
// 以编译期常量形式返回数组尺寸
// 此时入参是const int(&)[9],按照模式匹配规则,此时N为9
constexpr std::size_t arraySize(T(&)[N]) noexcept{
std::cout << N << std::endl;
return N;
}
int main()
{
int key[] = { 1,2,3,4,5,6,7,8,9 };
int new_key[arraySize(key)]; //此时key被推导为 const int(&)[9],返回长度为9
return 0;
}
同理,函数也是一样,具体逻辑见代码:
int someFunc(int,double);//someFunc是一个函数,类型是:void(int,double)
template <class T>
void f1(T param); //按值传递
template <class T>
void f2(T& param); //按引用传递
f1(someFunc); //此时param被推导为函数指针:void(*)(int,double)
f2(someFunc); //此时param被推导为函数引用:void(&)(int,double)