对于静态语言来讲,你通常要明确告诉编译器变量或者表达式的类型。可是庆幸地是,如今C++已经引入了自动类型推断:编译器能够自动推断出类型。在C++11
以前,类型推断只是用在模板上。而C++11
经过引入两个关键字auto
和decltype
扩展了类型推断的应用。C++14
更进一步扩展了auto
和decltype
的应用范围。明显地,类型推断能够减小不少无必要的工做。可是高兴之余,你仍然有可能会犯一些错误,若是你不能深刻理解类型推断背后的规则与机理。所以,咱们分别从模板类型推断、auto
和decltype
的使用三个方面深刻讲解类型推断。数组
模板类型推断在C++98
中就已经引入了,它也是理解auto
与decltype
的基石。下面是一个函数模板的通用例子:函数
template <typename T> void f(ParamType param); f(expr); // 对函数进行调用
编译器要根据expr
来推断出T
与ParamType
的类型。特别注意的是,这两个类型有可能并不相同,由于ParamType
可能会包含修饰词,好比const
和&
。看下面的例子:ui
template <typename T> void f(const T& param); int x = 0; f(x); // 使用int类型调用函数
此时类型推断结果是:T
的类型是int
,可是ParamType
的类型倒是const int&
。因此,两个类型并不相同。还有,你可能很天然地认为T
的类型与表达式expr
是同样的,好比上面的例子:二者是同样的。可是实际上这也是误区:T
的类型不只取决于expr
,也与ParamType
牢牢相关。这存在三种不一样的情形:spa
最简单的状况ParamType
是指针或者引用类型,但不是通用引用类型(&&)。此时,类型推断要点是:指针
expr
是引用类型,那就忽略引用部分;expr
与ParamType
的类型来决定T
的类型。好比,下面是引用类型的例子:code
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&类型 f(x); // 此时T为int,而param是int& f(cx); // 此时T为const int,而param是const int& f(rx); // 此时T为const int,而param是const int&
其中能够看到,const对象传递给接收T&
参数的函数模板时,const属性是可以被T
所捕获的,即const称为T
的一部分。同时,引用类型对象的引用属性是能够忽略的,并无被T
所捕获。上面处理的实际上是左值引用,对于右值引用,规则是相同的,可是右值引用的通配符T&&
还有另外的含义,会在后面讲。对象
若是param
是常量引用类型,推断也是类似的,尽管有些区别:索引
template <typename T> void f(const T& param); // param是常量引用类型 int x = 27; // x是int类型 const int cx = x; // cx是const int类型 const int& rx = x; // rx是const int&类型 f(x); // 此时T为int,而param是const int& f(cx); // 此时T为int,而param是const int& f(rx); // 此时T为int,而param是const int&
指针类型也一样适用:ip
template <typename T> void f(T* param); // param是指针类型 int x = 27; // x是int int* px = &x; // px是int* const int* cpx = &x; // cpx是const int* f(px); // 此时T是int,而param是int* f(cpx); // 此时T是const int,而param是const int*
显然,这种情形类型推断很容易。get
这种情形有点复杂,由于通用引用类型参数与右值引用参数的形式是同样的,可是它们是有区别的,前者容许左值传入。类型推断的规则以下:
expr
是左值,T
和ParamType
都推导为左值引用,尽管其形式上是右值引用(此时仅把&&匹配符,一旦匹配是左值引用,那么&&能够忽略了)。expr
是右值,能够当作情形1的右值引用。规则有点绕,仍是例子说话:
template <typename T> void f(T&& param); // 此时param是通用引用类型 int x = 10; // x是int const int cx = x; // cx是const int const int& rx = x; // rx是const int& f(x); // 左值,T是int&,param是int& f(cx); // 左值,T是const int&,param是const int& f(rx); // 左值,T是const int&,param是const int& f(10); // 右值,T是int,而param是int&&
因此,只要区分开左值与右值传入,上面的类型推断就清晰多了。
若是ParamType
既不是引用类型,也不是指针类型,那就意味着函数的参数是传值了:
template <typename T> void f(T param); // 此时param是传值方式
传值方式意味着param
是传入对象的一个新副本,相应地,类型推断规则为:
expr
类型是引用,那么其引用属性被忽略;expr
的引用特性后,其是const类型,那么也忽略掉。下面是例子:
int x = 10; // x是int const int cx = x; // cx是const int const int& rx = x; // rx是const int& f(x); // T和param都是int f(cx); // T和param仍是int f(rx); // T和param还是int
其实上面的规则不难理解,由于param
是一个新对象,不论其如何改变,都不会影响传入的参数,因此引用属性与const属性都被忽略了。可是有个特殊的状况,当你送入指针变量时,会有些变化:
const char* const ptr = "Hello, world"; // ptr是一个指向常量的常量指针 f(ptr);
尽管仍是传值方式,可是复制是指针,固然改变指针自己的值不会影响传入的指针值,因此指针的const属性能够被忽略。可是指针指向常量的属性却不能忽略,由于你能够经过指针的副本解引用,而后就修改了指针所指向的值,原来的指针指向的内容也会跟着变化,可是原来的指针指向的是const对象。矛盾会产生,因此这个属性没法忽略。所以,ptr的类型是const char*
。
尽管前面三种状况已经包含了可能,可是对于特定函数参数,仍然会有特殊状况。第一状况是传入的参数是数组,咱们知道若是函数参数是数组,其是当作指针来处理的,因此下面的两个函数声明是等价的:
void fun(int arr[]); // 数组形式 void fun(int* arr); // 指针形式 // 二者是等价的
因此,对于函数模板类型推断来讲,数组参数推断的也是指针类型,好比传值方式:
template <typename T> void f(T param); // 传值方式 const char[] name = "Julie"; // name是char[6]数组 f(name); // 此时T和param是const char*类型
可是若是是引用方式,事情就发生了变化,此时数组再也不被当作指针类型,而就是固定长度的数组。因此:
template <typename T> void f(T& param); // 引用类型 const char[] name = "Julie"; // name是char[6]数组 f(name); // 此时T是const char[6],而param类型是const char (&)[6]
显然与传值方式不一样,很难让人理解,可是事实就是如此。可是这也暴漏了一个事实:数组的引用利用函数模板能够推导出数组的大小,下面是一个能够返回数组大小的函数实现:
template <typename T, std::size_t N> constexpr std::size_t arraySize(T (&)[N]) noexcept { // 因为并不实际须要数组,只用到其类型推断,因此不须要参数 return N; } int arr[] = {1, 3, 7, 2, 9}; const int size = arraySize(arr); // 5
真实很神奇的一个函数,可是一切又合情合理!
另一个特殊状况就是传递的参数是函数,其实也是当作指针,和数组参数相似:
template <typename T> void f1(T param); // 传值方式 template <typename T> void f2(T& param); // 引用方式 void someFun(int); // 类型为void (int) f1(someFun); // T和param是 void (*) (int)类型 f2(someFun); // T是void (int)(不是指针类型),但param是void (&) (int)类型 // 尽管如此,实际使用时差异不大,用于回调函数时,通常不会去修改那个函数吧
C++11
引入了auto
关键字,用于变量定义时的类型自动推断。从表面上看,auto
与模板类型推断的做用对象是不同的。可是二者其实是一致的,函数模板推断的任务是:
template <typename T> void f(ParamType param); f(expr); // 根据expr类型推导出T和ParamType的类型
编译器要根据expr类型推导出T和ParamType的类型。移植到auto
上是那么容易:把auto
当作函数模板中的T,而把变量的实际类型当作ParamType。这样咱们能够把auto
类型推断转换成函数模板类型推断,仍是例子说话:
// auto推断例子 auto x = 10; const auto cx = x; const auto& rx = x; // 传化为模板类型推断 template <typename T> void f1(T param); f1(10); template <typename T> void f2(const T param); f2(x); template <typename T> void f3(const T& param); f3(x);
显然,很容易推断出各个变量的类型。前面说到,函数模板类型推断有三种状况,那么对于auto
来讲,仍然有三种情形:
下面是具体例子:
const int N = 2; auto x = 10; // 情形3: int const auto cx = x; // 情形3: const int const auto& rx = x; // 情形1:const int& auto y = N; // 情形3: int // 情形2 auto&& y1 = x; // 左值:int& auto&& y2 = cx; // 左值: const int& auto&& y3 = 10; // 右值:int&&
能够看到,auto
与函数模板类型推断本质上是一致的。可是有一个特殊状况,那就是C++11
支持统一初始化方式:
// 等价的初始化方式 int x1 = 9; int x2(9); // 统一初始化 int x3 = {9}; int x4{9};
上面的4种方式均可以用来初始化一个值为9的int变量,那么你可能会想下面的代码是一样的效果:
auto x1 = 9; auto x2(9); auto x3 = {9}; auto x4{9};
可是实际上不是这样:对于前两个,确实是初始化了值为9的int类型变量,可是后二者确是获得了包含元素9的std::initialzer_list<int>
对象(初始化列表),这算是auto
的一个特例吧。可是这对函数模板类型推断并不适用:
auto x = {1, 3, 5} // 合法:std::initializer_list<int>类型 template<typename T> void f(T param); f({1, 3, 5}); // 非法,没法编译:不能推断出T的类型 // 能够修改为下面 template <typename T> void f2(std::initializer_list<T> param); f2({1, 3, 5}); // 合法:T是int,param是std::initializer_list<int>
上面讲的都是关于auto
用于变量定义时的类型推断。可是C++14
中auto
还能够用于函数返回类型的推断以及泛型lambda
表达式(其参数支持自动推断类型)。以下面的例子:
// C++14功能 // 定义一个判断是否大于10的泛型lambda表达式 auto isGreaterThan10 = [] (auto i) { return i > 10;}; bool r = isisGreaterThan10(20); // false // auto用于函数返回类型自动推断 auto multiplyBy2Lambda(int x) { return [x] {return 2 * x;}; } auto f = multiplyBy2Lambda(4); cout << f() << endl; // 8
这些例子是auto
用于模板类型推断,不一样于前面的定义变量时的类型推断,不能使用初始化列表来推断:
// 如下都是没法编译的 auto createList() { return {1, 3, 5}; } auto f = [](auto v) {}; f({1, 3, 5});
总之,auto
与模板类型推断是一致的,除了要注意初始化列表这种特殊状况。
decltype
用于返回某一实体(变量名与表达式)的类型。咱们从最简单的例子开始:
const int x = 0; // decltype(x)是const int struct Point {int x; int y;}; Point p{2, 5}; // decltype(Point::x)是int; decltype(p.x)是int bool f(int x); // decltype(f)是bool(int) // decltype(f(2.0))是bool vector<int> v{2, 5}; // decltype(v)是vector<int> // decltype(v[0])是int&
大部分状况,decltype
按照你所预料的方式工做:decltype
用于一个变量名时,返回的正是该变量所对应的类型;用于函数返回值也正是函数返回值类型。可是当用于左值表达式时,decltype
推断出的类型却必定是一个引用类型,看下面的例子:
int x = 10; // decltype(x)是int,可是decltype((x))确是int& struct A {double x;}; const A* a = new A{2.0}; // decltype(a->x)是double,可是decltype((a->x))确是const double&
让人感受很是奇怪。其实普遍的C++表达式(字面值,变量名,表达式等等)包含两个独立的属性:类型(type)和值种类(value category)。这里的类型指的是非引用类型,而值种类有三个基本类型:xvalue
,lvalue
和prvalue
。当decltype
做用于不一样值种类的表达式上,其效果不同。具体能够参考这里(反正有点复杂)。
上面的简单了解就好,由于用的并非太多。而decltype
的一个很重要的应用是在函数模板中的返回值类型推断。这里举个例子:你想写一个函数,这个函数接收两个参数,一个支持索引操做符的容器对象,一个是索引参数;函数验证用户身份,而后返回值这个容器对象在该索引值处的元素,要求其返回类型与容器对象索引操做返回值类型同样。此时就可使用decltype
,先看一下下面的实现:
// C++11 template <typename Container, typename Index> auto authAndAccesss(Container& c, Index i) ->decltype(c[i]) { // 验证用户 // ... return c[i]; }
这种实现使用了C++11
中的“拖尾返回类型”:函数返回类型要在参数列表以后声明(使用->分割),使用“拖尾返回类型”,咱们能够利用函数的参数来推断返回类型:上面就用了c[i]
来推断返回值类型。还有注意的是上面的auto
没有推断功能,仅仅是指明使用了“拖尾返回类型”。你们可能会想,为何不把decltype(c[i])
直接替换auto
的位置?这样是不行的,由于此时函数参数尚未被建立!
可是C++14
容许你省略掉拖尾部分:
// C++14 template <typename Container, typename Index> auto authAndAccesss(Container& c, Index i) { // 验证用户 // ... return c[i]; }
此时仅留下auto
,此时auto
真正用于返回值类型推断:即根据返回值表达式c[i]
来推断返回类型。此时,问题来了。咱们知道容器的索引操做返回的大部分是引用类型,可是auto
推导类型时,会忽略c[i]
的引用属性,那么函数返回值是一个右值(尽管咱们但愿它仍然是左值),下面的代码就存在问题:
vector<int> v{1, 2, 3, 4, 5}; authAndAccess(v, 2) = 10; // 没法编译:没法对右值赋值
咱们知道decltype(c[i])
是能够正常推断的,因此,为了解决上面的问题,C++14
引入了decltype(auto)
标识符:auto
说明类型须要推断,decltype
说明类型推断要使用decltype
规则。因此,再次修改代码:
template <typename Container, typename Index> decltype(auto) authAndAccesss(Container& c, Index i) { // 验证用户 // ... return c[i]; }
此时,若是c[i]
的返回类型是引用类型,那么函数的返回类型也是引用类型。其实decltype(auto)
还能够用于声明变量:
int x = 10; const int& cx = x; auto y = cw; // 类型是int decltype(auto) z = cw; // 类型是const int&
对于修改版本的authAndAccesss,一个问题你只能传递左值引用的容器对象,而且该对象不能是常量左值引用。可是咱们想既能够传递左值又能够传递右值,这个时候你须要使用&&
通用引用:
template <typename Container, typename Index> decltype(auto) authAndAccesss(Container&& c, Index i) { // 验证用户 // ... return std::forward(c)[i]; }
其中std::forward
函数是专门处理通用引用类型参数的,基本上就是传入的参数是右值,转化的仍是右值引用,若是是左值,那么转化的是左值引用,具体能够参考这里。