考虑这样的函数模板
template<typename T>
void f(ParamType param);
f(expr); //从expr中推导T和ParamType
有三种情况需要考虑
情况一:ParamType是一个指针或引用,但不是通用引用
最简单的情况是ParamType
是一个指针或者引用,但非通用引用。在这种情况下,类型推导会这样进行:
- 如果
expr
的类型是一个引用,忽略引用部分 - 然后
expr
的类型与ParamType
进行模式匹配来决定T
比如模板是这样的
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的x的引用
f(x); // T是int, param的类型是int&
f(cx); // T是const int, param的类型是const int&
f(rx); // T是const int, param的类型是const int&
这也是为什么将一个const
对象传递给以T&
类型为形参的模板安全的:对象的常量性constness会被保留为T
的一部分
如果是右值也是一样的
如果我们将f
的形参类型T&
改为const T&
,情况有所变化
template<typename T>
void f(const T& param); // param现在是reference-to-const
int x = 27; // 跟之前一样
const int cx = x; // 跟之前一样
const int& rx = x; // 跟之前一样
f(x); // T是int,param的类型是const int&
f(cx); // T是int,param的类型是const int&
f(rx); // T是int,param的类型是const int&
因为现在我们假设param
是reference-to-const,const
不再被推导为T
的一部分
如果是指针,本质上也是一样的
template<typename T>
void f(T* param); // param现在是指针
int x = 27; // 同之前一样
const int *px = &x; // px是指向作为const int的x的指针
f(&x); // T是int,param的类型是int*
f(px); // T是const int,param的类型是const int*
情况二:ParamType是一个通用引用
- 如果
expr
是左值,T
和ParamType
都会被推导为左值引用。第一,这是模板类型推导中唯一一种T
被推导为引用的情况。第二,虽然ParamType
被声明为右值引用类型,但是最后推导的结果是左值引用。 - 如果
expr
是右值,就使用正常的(也就是情况一)推导规则
template<typename T>
void f(T&& param); //param现在是一个通用引用类型
int x = 27;
const int cx = x;
const int & rx = cx;
f(x); // x是左值,所以T是int&,
// 引用折叠,int& && 折叠成 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&&
情况三:ParamType既不是指针也不是引用
template<typename T>
void f(T param); //以传值的方式处理param
- 和之前一样,如果
expr
的类型是一个引用,忽略这个引用部分 - 如果忽略
expr
的引用性(reference-ness)之后,expr
是一个const
,那就再忽略const
。如果它是volatile
,也忽略volatile
(volatile
对象不常见,它通常用于驱动程序的开发中
因此
int x = 27;
const int cx = x;
const int& rx = cx;
f(x); // T和param的类型都是int
f(cx); // T和param的类型都是int
f(rx); // T和param的类型都是int
具有常量性的cx
和rx
不可修改并不代表param
也是一样,因为param
只是它们的拷贝
只有在传值给形参时才会忽略const
(和volatile
)
考虑指针的顶层const和底层const情况
template<typename T>
void f(T param); //仍然以传值的方式处理param
// ptr是一个常量指针,指向常量对象
const char* const ptr = "Fun with pointers";
f(ptr); // 传递const char * const类型的实参,param的类型是const char *
ptr
自身的常量性将会被省略,也就是顶层的const
被忽略,底层的const
被保留,所以param
的类型是const char*
,不能改变所指向的对象
数组实参
数组可以退化为指针
const char name[] = "J. P. Briggs"; // name的类型是const char[13]
const char* ptrToName = name; // 数组退化为指针
这两种类型(const char*
和const char[13]
)是不一样的
如果给一个函数传递数组,形参可以是这样的
void myFunc(int param[]);
但是数组声明会被视作指针声明,这意味着myFunc
的声明和下面声明是等价的
void myFunc(int* param);
因为数组形参会视作指针形参,所以传值给模板的一个数组类型会被推导为一个指针类型
template<typename T>
void f(T param); // 传值形参的模板
f(name); // name是一个数组,但是T被推导为const char*
但是现在难题来了,虽然函数不能声明形参为真正的数组,但是可以接受指向数组的引用
template<typename T>
void f(T& param); //传引用形参的模板
f(name); // T被推导为const char[13],所以param是const char(&)[13]
这样也可以创建一个模板函数来推导出数组的大小
//在编译期间返回一个数组大小的常量值
template<typename T, size_t N>
constexpr size_t arraySize(T (&)[N]) noexcept
{
return N;
}
函数实参
函数类型也会退化为一个函数指针
void someFunc(int, double); // someFunc是一个函数,
// 类型是void(int, double)
template<typename T>
void f1(T param); // 传值给f1
template<typename T>
void f2(T& param); // 传引用给f2
f1(someFunc); // param被推导为指向函数的指针,
// 类型是void(*)(int, double)
f2(someFunc); // param被推导为指向函数的引用,
// 类型是void(&)(int, double)
请记住:
- 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
- 对于通用引用的推导,左值实参会被特殊对待
- 对于传值类型推导,
const
和volatile
实参会被认为是non-const的和non-volatile的 - 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用