本文参考Effective Morden C++ C++98只有一种类型推导规则:函数模板。而C++11改变了其中的一些规则,并添加了另外两种类型推导,他们是auto和decltype。C++14又有了进一步的扩充。 一、首先来看函数模板中的类型推导: 一般函数模板的原型如下:
template<typename T> void f(ParamType param);调用代码如下:
f(expr);编译器编译时通过expr推导出T的类型和ParamType的类型。ParamType和T的区别可能包括一些修饰,例如:const或引用特性等。根据ParamType的形式不同,可分为以下三类:
ParamType是一个指针或者左值引用类型; 如果expr是一个类型的引用,则忽略引用的部分,然后用expr的类型和ParamType对比来判断T的类型。下面仅以引用为例,指针和引用类似。模板代码如下: 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); //同上分析:对于第二、第三行来说,由于cx和rx被指定为const类型,则T类型为const int。对象的const特性是T类型推导的一部分。第三行中,rx本身的引用特性被忽略。 而如果我们直接用cosnt修饰param,如下所示:
template<typename T> void f(const T& param);则T的类型推导中不会有const。指针和引用类似,可自行推导,此处不再赘述。 2. ParamType是一个右值引用: 此时,如果expr是一个左值,则T和ParamType都会被推导为左值引用(引用折叠),这也是T被推导成引用的唯一情况。如果expr是一个右值,则情况很简单。 例子如下:
template<typename T> void f(T&& param); int x = 27; const int cx = x; const int& rx = x; f(x); //x是左值,所以T和param都是int& f(cx); //cx是左值,所以T和param都是const int& f(rx); //同上 f(27); //27是右值,所以T是int, param是int&& ParamType既不是指针也不是引用: 此处为按值传递,形参param仅为实参的一个拷贝,则会忽略掉实参的引用,const,volatile等属性。一个很好的例子如下: template<typename T> void f(T param); const char* const ptr = "LaLa"; f(ptr);第一个const表示ptr指向的变量的值不能通过ptr来修改,第二个const表示ptr不能指向其他的变量。由于f(ptr)调用时为按值传递,故第二个const失效,在函数f中可以使param指向其他的变量。 除此之外,还有两种特殊情况需要说明。 1)数组参数: 数组类型和指针类型是有区别的,虽然很多时候数组可以退化成指向其第一个元素的指针。而且函数的数组参数声明会被当做是指针参数,更加深了数组名会被当做指针的错觉。
const char name[] = "LaLaLa!" template<typename T> void f(T param); f(name); //虽然name是数组,但是T类型为const char*传递给模板函数的按值传递的数组参数会退化成指针类型。如果我们把模板f的参数改为引用,则会出现不同的结果。
template<typename T> void f(T& param); f(name); //T类型为const char[13],是数组类型;2)函数参数 函数类型也可以退化成函数指针。代码如下:
void Func(int); template<typename T> void f1(T param); template<typename T> void f2(T& param); f1(Func); //param被推导成函数指针,类型为void(*)(int) f2(Func); //param被推导成函数指针,类型为void(&)(int) 二、auto的类型推导只有一个例外,auto推导就是函数模板类型推导。模板类型推导和auto类型推导是有一个直接映射的:当变量被声明为auto,auto就相当于模板中的T,而对变量做的相关的类型限定就像ParamType。对待花括号{}初始化的行为是auto和模板类型推导唯一不一样的地方。
auto x = {1, 2, 3}; //x类型为std::initializer_list<int> template<typename T> void f(T param); f({1, 2, 3}); //错误,无法推导T的类型。C++14允许auto表示推导函数返回值,而且lambda可以在参数声明里面使用auto。但是这里面的auto类型推导是复用了模板的类型推导,而不是auto的类型推导。 三、decltype的类型推导 decltype的原则是给其一个变量或者表达式,反馈给你变量或表达式的类型。这不但易于操作,而且对于绝大多数情况下结果都很容易理解。在C++11中,decltype最主要的用处是用来声明一个模板,模板中返回值得类型取决于参数的类型,无法直接给出,可以使用decltype的尾置返回类型。auto相当于一个占位符,实际类型推导在其后面的decltype处。实例代码如下:
template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) -> decltype(c[i]) { authenticateUser(); return c[i]; }这将得到我们想要得到的结果。C++11允许单据的lambda表达式的返回类型被推导,在C++14中行为被拓展到多语句的所有的lambda表达式和函数。这就意味着在C++14中可以忽略尾随返回类型,仅保留开头的auto。这样就可以使用下面的代码:
template<typename Container, typename Index> auto authAndAccess(Container& c, Index i) { authenticateUser(); return c[i];上面的代码会由于auto在推导时会忽略引用和const属性而带来一些问题。所以在C++14中我们将auto和decltype组合在一起,auto指定要推导的类型,decltype则指定推导时使用decltype的规则。代码如下:
template<typename Container, typename Index> decltype(auto) authAndAccess(Container& c, Index i) { authenticateUser(); return c[i];对于一个变量名使用decltype得到这个变量名的声明类型。然而,对于一个比变量名更复杂的左值表达式,则会返回类型的左值引用。例如这样:
int x = 0; decltype(x) //类型为int decltype((x)) //类型为int&这样,下面的代码将会带来一些问题。
decltype(auto) f1() { int x = 1; ... return x; } decltype(auto) f2() { int x = 0; ... return (x); }调用f2将会返回局部变量的引用而带来问题。