从一个基础的函数模板开始:
template<typename T>
void f(ParamType param);
// 伪代码,ParamType通常是T加上装饰符(adornments,如const, &, *等)
简单具化ParamType:
template<typename T>
void f(const T& param);
int x = 0;
f(x); // T被推导为int
看起来,T
只根据 expr
推导。然而实际上,T
还 与 ParamType
的形式有关,分三种情况:
1. ParamType
是指针或引用类型,但不是万能引用(universal reference,详见Item 24,现在只需知道它是种和左值右值都不同的引用)。
2. ParamType
是万能引用。
3. ParamType
既不是指针也不是引用。
Case 1. ParamType
是指针或引用类型,但不是万能引用
- 此时,类型推导的规则为:
- 如果
expr
的类型是引用,忽略引用部分。 - 然后将
expr
的类型匹配到ParamType
上来决定T
的类型。
- 如果
举例:
template<typename T>
void f(T& param); // 模板的ParamType为引用
int x = 27; // x是int
const int cx = x; // cx是const int
const int& rx = x; // rx是对x的常引用
f(x); // T是int, param是int&
f(cx); // T是const int, param是const int&
f(rx); // T是const int, param是const int&
VS2022运行结果:
(关于如何打印正确的类型名,详见Item 4。实测VS中使用typeid(T).name()会丢失类型的const信息)
- 由上可见向
T&
参数的函数传一个const
对象是安全的,因为const
性质会被保留到推导出的T
的类型中。 - 如果将
ParamType
由T&
改为const T&
,三个调用中T
均被推导为int
。 - 指针与引用的情况基本相同。
Case 2. ParamType
是万能引用
- 万能引用参数的声明方式类似右值引用(
T&&
),当传入左值时其表现不同。Item 24讲述了完整原理,这里给出简单总结:- 如果
expr
是左值,T
和ParamType
都被推导为左值引用。这是T
会被推导为引用的唯一情况,而且尽管ParamType
使用右值语法声明,其推导类型仍为左值引用。 - 如果
expr
是右值,按照Case 1正常规则。
- 如果
举例:
template<typename T>
void f(T&& param); // param是万能引用
int x = 27;
const int cx = x;
const int& rx = x;
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&&
VS2022运行结果:
Case 3. ParamType
既不是指针也不是引用
- 此时我们面对的是传值问题,这意味着
param
会是传入对象的一个复制,一个全新的对象。- 如果
expr
的类型是引用,忽略引用部分。 - 如果忽略引用后
expr
是const
或volatile
,把那部分也忽略掉。(关于volatile
,详见Item 40)
- 如果
举例:
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T和param的类型都是int
f(cx); // T和param的类型也都是int
f(rx); // T和param的类型还都是int
VS2022运行结果:
- 尽管入参
cx
和rx
是const
值,param
并不是const
,因为它已经是和前者完全无关的新对象了。volatile
同理。 - 考虑指向常量对象的常指针:
template<typename T>
void f(T param);
// char*左侧的const说明ptr指向的对象本身(即那个字符串)不能被改变
// 右侧的const说明ptr不能指向别的对象或被设为null
const char* const ptr = "Fun with pointers";
f(ptr);
- 此时,
ptr
被按字节复制到param
中,使其右侧的const
被忽略,而保留左侧。即param
是指向的对象本身不可修改,但指向什么对象可以修改的指针(const char*
)。
数组参数
- 数组类型参数与指针类型不是完全相同的。
- 在类型推导中,传入的数组变量的确会被推导为指针类型:
template<typename T>
void f(T param);
const char name[] = "J. P. Briggs";
f(name); // name是数组,但推导的类型是const char*
- 有趣的是,如果将参数声明为对数组的引用,这时推导出的类型将是数组的真实类型!
template<typename T>
void f(T& param);
const char name[] = "J. P. Briggs";
f(name); // T被推导为const char[13], param被推导为const char (&)[13]!
VS2022运行结果:
- 利用这个特性甚至可以写出一个编译期的萃取数组长度的模板函数:
// constexpr见Item 14,noexcept见Item 15
// 这里我们只关注数组元素个数,所以不需要参数名
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
char name[] = "123"; // 返回4
int keyVals[] = {1, 2, 3}; // 适用于大括号初始化的数组,返回3
函数参数
- 函数类型的参数也会退化(decay)为函数指针。以上对于数组的讨论都适用于函数指针:
void someFunc(int, double);
// type is void(int, double)
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); //param类型为void (*)(int, double)
f2(someFunc); //param类型为void (&)(int, double)
总结
- 在模板类型推导中,入参的引用修饰符会被忽略。(reference-ness is ignored)
- 推导万能引用参数时,左值入参会被特殊处理。
- 推导传值参数时,
const
和volatile
入参会被当做非const
volatile
变量处理(因为对象在传递时已经被复制)。 - 在模板类型推导中,数组或函数入参会退化为指针,除非模板中使用引用参数。