函数模板和类模板是C++泛型编程的基础,它使得我们的程序不再受数据类型的限制。因此,要写出高效的现代C++程序,理解模板类型推导是很有必要的。
下面是一个典型的函数模板:
template<typename T>
void func(ParamType param); // ParamType可以是T、const T、const T&等
// ParamType可以说是用const、&等包装之后的T
其中ParamType
可以是T
、const T
、const T&
等。
调用如下:
func(expr); // expr = expression
在编译的时候,编译器通过expr
来对ParamType
和T
进行推导(通常来说ParamType和T不同,如ParamType为const T、const T&等)。
例如:
template
void func(const T& param);
...
int x = 0;
f(x);
此时,T
被推导为int
,ParamType
被推导成const int&
。
实际上,C++的模板类型推导远不止上述这么简单,T
的类型不仅与expr
的类型有关,还与ParamType
的形式有关。
下面分三种情况讨论模板类型推导:
1. ParamType是一个指针或者是一个非通用引用
(其中通用指针的概念后面会进行解释)
此时,T
的推导步骤如下:
expr
的类型是引用,则忽略引用符号。expr
与ParamType
进行匹配来判断T
的类型。例如:
template<typename T>
void func(T& param);
...
int x = 27;
const int cx = x;
const int& rx = x;
...
f(x); // x为int,因此param的类型为int&而T为int
f(cx); // cx为const int,因此param的类型为const int&而T为const int
f(rx); // rx为const int&,忽略引用后为const int,因此param的类型为const int&而T为const int
上述代码有如下几个需要注意的地方:
f(rx)
中忽视了rx
的引用特性,这个在步骤1中已经说明了,因此没有什么需要额外解释的。expr
与ParamType
是如何匹配来判断T
的类型还没有解释清楚。看下面例子:template<typenameT>
void func1(T& param);
template<typeanmeT>
void func2(const T& param);
int x = 27;
const int cx = x;
func1(cx); // cx的类型为const int,对比T&得到T为const int
func2(cx); // cx的类型为const int,对比const T&得到T为int
上述代码中,func1
和func2
的不同在于ParamType
,分别为T&
和const T&
。
const int
与T&
对比得到T
为const int
;const int
与const T&
对比得到T
为int
。同样的,当ParamType
为指针时也遵循上述规则:
template<typename T>
void func(T* param);
int x = 27;
const int *px = &x;
func(px); // px为const int *对比T*得到T为const int,从而param的类型为const int *
上述代码中,ParamType
为T*
,而expr
为const int*
,因此T
被推导为const int
,从而得到param
的类型为const int *
。
2. ParamType是一个通用引用(Universal Reference)
一个通用引用参数的申明类型是T&&
,此时需要根据expr
是左值还是右值分情况讨论:
expr
是一个左值,T
和ParamType
都被推导成左值引用。expr
是一个右值,则执行和情况1相同的法则。例如:
template<typename T>
void func(T&& param);
int x = 27;
const int& rx = x;
f(x); // 由于x为左值,因此T和param都被推导为int& -- ①
f(rx); // 由于rx为左值,因此T和param都被推导为const int& -- ①
f(27); // 由于27是右值,其类型为int,与T&&进行对比得到T类型为int而param类型为int&& -- ②
上述代码中,f(rx)
中rx
的类型为const int&
,首先忽略引用,然后推导为左值引用。f(27)
中27
为右值,因此推导为int&&
。
3. ParamType不是指针或引用
当ParamType
既不是指针也不是引用时,将其处理为传值的形式。这意味着param
是参数的一份拷贝,是一个完全新的对象。
同样的,根据expr
不同情况给出推导的法则:
expr
是一个引用,则忽略引用部分。expr
是个const
或者volatile
,也要忽略掉const
或volatile
例如:
template<typename T>
void func(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x类型为int,因此T和param的类型也为int
f(cx); // cx类型为const int,忽略掉const,因此T和param的类型仍为int
f(rx); // rx类型为const int&,忽略掉const和&,因此T和param的类型还是int
考虑expr
是一个const指针指向一个const对象,而expr
是按值传递的方式传递给func
:
template
void f(T param);
const char* const ptr = "Hello World!";
f(ptr); // ptr是一个指向const对象的const指针,忽略其本身的const特性后为const char*
// 因此可以推导出T和param的类型为const char*
上述代码中,const char*
表示ptr
指向一个常字符数组,而const ptr
表示ptr
是一个常指针,自初始化之后便不能变更。因此,在推导的过程中忽略了ptr
的const
属性而保留了其指向对象的const
属性,最终推导出T和param的类型为const char*
。
此外,以数组或者函数作为参数进行模板的规则如下:
1. 数组参数
在大多数人眼中,数组类型和指针类型几乎是一样的,如下:
const char str[] = "Hello World!"; // str的类型为const char[13]
const char *ptr = str; // ptr的类型为const char*
实际上,str
的类型为const char[13]
,而ptr
的类型为const char*
。二者的区别在于str
是一个数组对象,而ptr
是一个指针对象,用sizeof关键字作用于二者时,sizeof(str)
返回13,而sizeof(ptr)
返回4(32位).
因此,代码const char * ptr = str
过程实际上是数组退化为指针的过程。
对于下面两个函数声明:
void func1(int param[]);
void func2(int* param);
实际上是等价的,因为C++会把数组声明退化为指针声明。这就导致了数组和指针等价的错觉。
尽管函数参数不能真正定义为数组,但是可以声明参数为数组的引用:
templaete<typename T>
void func(T& param);
const char str[] = "Hello World!";
func(str);
推导规则对应了上面的情况1,因此T
的类型为const char [13]
,而param
的类型为const char(&)[13]
因此,利用上述模板函数可以推导出数组的长度:
template<typename T>
int func(T& param){
return sizeof(param); // sizeof关键字的返回值类型为usnsigned_int64
} // 因此,用int作为返回值可能会溢出。
const char str[] = "Hello World!";
cout << func(str) << endl; // 13
下面给出一个高级严谨的写法:
template<typename T, std::size_t N>
constexpr std::size_t func(T (&)[N]) noexcept{
return N;
}
2. 函数参数
数组可以退化为指针,函数也可以退化为函数指针:
void someFunc(int, double);
template<typename T>
void func1(T param);
template<typename T>
void func2(T& param);
func1(someFunc); // param被推导为函数指针,类型为void(*)(int, double)
func2(someFunc); // param被推导为函数指针,类型为void(&)(int, double)
上述两种方式在实践中几乎没有区别。
综上,模板推导主要有以下几个关键点:
const/volatile
类型参数的const/volatile
属性被忽略。auto
类型推导几乎和模板类型推导的过程相同,其中唯一不同的一点会在后面提及。
例如:
auto x = 27;
和下面模板推导的规则相同:
template<typename T>
void func(T param);
const auto cx = x;
和下面模板推导的规则相同:
template<typename T>
void func(const T param);
const auto& rx = x;
和下面模板推导的规则相同:
template<typename T>
void func(const T& param);
可以看出,auto
实际上就等价于模板推导中的T
,因此const auto cx = x
推导出cx
的类型为const int
,而const auto& rx = x
推导出rx
的类型为const int&
。
同样的,对于情况2:
auto&& uref1 = x; // x(int)为左值,因此推导出uref1的类型为int&
auto&& uref2 = cx; // cx(const int)为左值,因此推导出uref2的类型为const int&
auto&& uref3 = 27; // 27为右值,因此推导出uref3的类型为int&&
对于数组和函数对象,auto
同样推导为指针:
const char str[] = "Hello World!";
auto arr1 = str; // arr1的类型为const char*
auto& arr2 = name; // arr2的类型为const char(&)[13]
void someFunc(int, double);
auto& func2 = someFunc; // func2的类型为void(&)(int, double)
auto唯一和模板推导不同的地方在于花括号初始化:当auto声明变量被使用一对花括号初始化,推导的类型是std::initializer_list
的一个实例,但如果将相同的初始化传递给相同的模板,则类型推导失败,代码不能通过编译。
例如:
auto x = {1, 2, 3} // x的类型为std::initializer_list
template<typename T>
void func(T param);
f({1, 2, 3}); // 错误!无法推导出T的类型
但是,当明确模板的param
的类型是一个未知类型T
的std::initializer_list
:
template<typename T>
void func(std::initializer_list<T> initList);
f({1, 2, 3}); // T被推导为int,initList的类型为std::initializer_list
因此,auto和模板类型推导的本质区别是auto假设花括号代表的是std::initailizer_list
,而模板推导则不是。
此外,当使用auto进行花括号的类型推导时,如果花括号中的数据类型不同,则类型推导也会失败:
auto x = {1, 2, 3.0}; // 无法推导"auto"类型
C++14允许auto
表示函数的返回值,并且允许在lambda
中的函数声明中使用auto
。
令人蛋疼的是,当使用auto
声明函数的返回值时,利用的却是模板的类型推导规则,而不是auto
的类型推导,例如下例无法编译:
auto createInitList(){
return {1, 2 ,3.0};
}
在VS中提示的错误为:以括号起的列表不提供返回类型。
同样的,在lambda
中使用auto
进行参数类型声明时也是利用的模板类型推导规则,下例同样无法编译:
std::vector<int> v;
auto restV = [&v](const auto& newValue){v = newValue;};
restV({1, 2, 3}); // 编译错误,无法推导出{1, 2, 3}的类型。
decltype
关键字返回给定变量或表达式的类型,并且可以用这个返回的类型来声明变量。
与auto
和模板推导不同的是,decltype
一般只是复述一遍所给的变量名或者表达式的类型,如下:
const int i = 0; // decltype(i) is const int
bool f(const Widget & w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
};
Widget w; // decltype(w) is Widget
if(f(w))... // decltype(f(w)) is bool
template<typename T>
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) is vector
...
if(v[0] == 0) // decltype(v[0])is int&
在C++11
中,decltype
最主要的用处之一就是用来声明一个函数模板,在这个函数模板中返回值的类型取决于参数的类型,如下例:
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
->decltype(c[i]) {
return c[i];
}
这里使用了C++11
的尾置返回类型技术,即函数的返回值类型在函数参数之后声明,其优势是在定义返回值类型的时候使用函数参数。在这个时候,auto
的类型推导没有起作用,而是由尾置返回指定了返回值的类型。
当利用auto
进行类型推导时:
template<typename Contianer, typename Index>
auto authAndAccess(Container& c, Index i) {
return c[i];
}
前面说到,当使用auto
来推导函数的返回类型时,使用的是模板推导的规则,那么对于返回值为&T
时会忽略引用,得到的仅为返回值的一份拷贝,如下例:
vector<int> v = {1, 2, 3, 4, 5, 6};
authAndAccess(v, 3) = 10; // 报错,因为返回值为右值
将auto
修改为auto&
则可以达到目的:
template<typename Container, typename Index>
auto& authAndAccess(Container& c, Index i){
return c[i];
}
vector<int> v = {1, 2, 3, 4, 5, 6};
authAndAccess(v, 3) = 10; // v = {1, 2, 3, 10, 5, 6}
在C++14
中,通过decltype(auto)
可以更好的实现上述功能:auto
指定需要推导的类型,decltype
表明在推导的过程中使用decltype
推导规则,因此可以重写如下:
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i) {
return c[i];
}
decltype(auto)
并不仅限于使用在函数返回值类型上,它也可以很方便的来声明一个变量:
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction
// myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction
// myWidget2's type is const Widget&
综上,本文总结了C++的部分模板推导规则,其中大多数规则自C++11之后才支持,少量规则仅C++14支持。这些模板推导规则虽然很复杂,但却是C++泛型变成的基础,只有理解了这些模板推导规则才能写出高效的代码。
参考:《Effective Modern C++》