一、介绍
对于一个复杂系统的用户来说,很多时候他们最关心的是它做了什么而不是它怎么做的。在这一点上,C++中的模板类型推导表现得非常出色。数百万的程序员只需要向模板函数传递实参,就能通过编译器的类型推导获得令人满意的结果,尽管他们中的大多数在被逼无奈的情况下,对于传递给函数的那些实参是如何引导编译器进行类型推导的,也只能给出非常模糊的描述。
如果那些人中包括你,我有一个好消息和一个坏消息。好消息是现在C++最重要最吸引人的特性auto
是建立在模板类型推导的基础上的。如果你满意C++98的模板类型推导,那么你也会满意C++11的auto
类型推导。坏消息是当模板类型推导规则应用于auto
环境时,有时不如应用于template时那么直观。由于这个原因,真正理解auto
基于的模板类型推导的方方面面非常重要。这项条款便包含了你需要知道的东西。
如果你不介意浏览少许伪代码,我们可以考虑像这样一个函数模板:
template<typename T>
void f(ParamType param);
它的调用通常是这样的
f(expr)
在编译期间,编译器使用expr
进行两个类型推导:一个是针对T
的,另一个是针对ParamType
的。这两个类型通常是不同的,因为ParamType
包含一些修饰,比如const
和引用修饰符。举个例子,如果模板这样声明:
template<typename T>
void f(const T& param); // ParamType是const T&
调用的形式像下面这样:
int x = 0;
f(x); //用一个int类型的变量调用f
T
被推导为int
, param
被推导为const T&
。
我们可能很自然的期望T
和传递进函数的实参expr
是相同的类型,也就是,T
为expr
的类型。在上面的例子中,事实就是那样:x
是int
,T
被推导为int
。但有时情况并非总是如此,T
的类型推导不仅取决于expr
的类型,也取决于ParamType
的类型。这里分为三种情况
ParamType
是一个指针或引用,但不是万能引用,如T&
。ParamType
是一个万能引用引用,如T&&
。ParamType
既不是指针也不是引用,T
。
我们下面将分成三个情景来讨论这三种情况,每个情景的都基于我们之前给出的模板:
template<typename T>
void f(ParamType param);
f(expr); //从expr中推导T和ParamType
二、Case1:ParamType
是一个指针或引用,但不是通用引用
最简单的情况是ParamType
是一个指针或者引用,但非通用引用。在这种情况下,类型推导会这样进行:
- 如果
expr
的类型是一个引用,忽略引用部分 - 然后
expr
的类型与ParamType
进行模式匹配来决定T
我们的模板声明如下:
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
using std::cout;
using std::endl;
template<typename T>
void f(T& param){
cout << "T's type is " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
// 使用”type_id_with_cvr“来进行类型获取
我们声明如下变量:
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是const int &
调用的形式如下:
f(x); // T是int, ParamType是int&, param是int&
f(cx); // T是const int, ParamType是const int&, param是const int&
f(rx); // T是const int, ParamType是const int&, param是const int&
在第二个和第三个调用中,注意因为cx
和rx
被指定为const
值,所以T
被推导为const int
,从而产生了const int&
的形参类型。这对于调用者来说很重要。当他们传递一个const
对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const
的。这也是为什么将一个const
对象传递给以T&
类型为形参的模板安全的:对象的常量性constness
会被保留为T
的一部分。
在第三个例子中,注意即使rx
的类型是一个引用,T
也会被推导为一个非引用 ,这是因为rx
的引用性(reference-ness
)在类型推导中会被忽略。
如果我们将f
的形参类型T&
改为const T&
,情况有所变化,但不会变得那么出人意料。cx
和rx
的constness
依然被遵守,但是因为现在我们假设param
是reference-to-const
,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&
同之前一样,rx
的reference-ness
在类型推导中被忽略了。
如果param
是一个指针(或者指向const
的指针)而不是引用,情况本质上也一样:
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*
三、Case2:ParamType
是一个万能引用
万能引用参数的声明方式类似右值引用(T&&
),当传入左值时其表现不同。这里给出简单总结:
- 如果
expr
是左值,T
和ParamType
都被推导为左值引用。这是T
会被推导为引用的唯一情况,而且尽管ParamType
使用右值语法声明,其推导类型仍为左值引用,从而触发引用折叠int& && => int&
。 - 如果
expr
是右值,按照Case 1
正常规则
template<typename T>
void f(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //x是左值=>T是int&=>ParamType是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&&
四、Case3:ParamType
既不是引用也不是指针
此时我们面对的是传值问题,这意味着 param 会是传入对象的一个复制,一个全新的对象。
- 如果
expr
的类型是引用,忽略引用部分。 - 如果忽略引用后
expr
是const
或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
五、数组作为实参(expr)
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。 这种退化会导致下面这种代码是可编译的:
const char name[] = "jack";
const char* ptrToName = name;
但是如果将数组传递给下面这个模板会怎样呢?
template<typename T>
void f3(T param) {
cout << "T's type is " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
const char name[] = "jack";
f3(name); // T(const char*)
也就是说,这里无论传递数组还是指针都是等价的。
但是使用如下的模板便会区分出指针和数组的不同:
template<typename T>
void f2(T& param) {
cout << "T's type is " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
const char name[] = "jack";
const char* ptrToName = name;
int x = 10;
const int* px = &x;
f2(name); // T(const char [5]), Param(char const (&)[5])
f2(ptrToName); // T(const char*), Param(const char*&)
f2(px); // T(const int*), Param(const int*)
可以看到T
被推导为数组的类型,也就是expr
的类型, 形參(Param
)被推导为char const (&)[5]
类型。
这种引用数组的功能,可以让我们实现在编译期间推导出数组长度的功能:
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{ // 少见
return N;
}
const char name[] = "jack";
cout << arraySize(name) << endl; // 5
六、 函数作为实参(expr)
在c++
中不止数组类型会发生退化,函数类型同样会发生退化, 我们对于数组退化的讨论同样适用于函数:
template<typename T>
void f2(T& param) {
cout << "T's type is " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
template<typename T>
void f3(T param) {
cout << "T's type is " << type_id_with_cvr<T>().pretty_name() << endl;
cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
void someFunc(int, double){}
f2(someFunc); // T(void (int, doble)), Param(void (&)(int, double))
f3(someFunc); // T(void (*)(int, double)), Param(void (&)(int, double))
函数引用和函数指针都可以调用, 如
param(1, 1.1)
七、总结
- 在模板类型(
T
)推导时,有引用的实参(expr
)会被视为无引用,他们的引用会被忽略 - 对于通用引用的推导,左值实参会被特殊对待,左值会导致模板类型被推导为左引用(比如expr为
int
类型左值,那么T
会被推导为int&
) - 对于传值类型的推导,实参
expr
的const
和volatile
属性会被忽略 - 对与传值类型得模板推导,数组或者函数名会退化为指针