笔记-1-Deducing Type

C++类型推断

对于静态语言来讲,你通常要明确告诉编译器变量或者表达式的类型。可是庆幸地是,如今C++已经引入了自动类型推断:编译器能够自动推断出类型。在C++11以前,类型推断只是用在模板上。而C++11经过引入两个关键字autodecltype扩展了类型推断的应用。C++14更进一步扩展了autodecltype的应用范围。明显地,类型推断能够减小不少无必要的工做。可是高兴之余,你仍然有可能会犯一些错误,若是你不能深刻理解类型推断背后的规则与机理。所以,咱们分别从模板类型推断、autodecltype的使用三个方面深刻讲解类型推断。数组

模板类型推断

模板类型推断在C++98中就已经引入了,它也是理解autodecltype的基石。下面是一个函数模板的通用例子:函数

template <typename T> void f(ParamType param); f(expr); // 对函数进行调用 

编译器要根据expr来推断出TParamType的类型。特别注意的是,这两个类型有可能并不相同,由于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

情形1:ParamType是指针或者引用类型

最简单的状况ParamType是指针或者引用类型,但不是通用引用类型(&&)。此时,类型推断要点是:指针

  1. 若是expr是引用类型,那就忽略引用部分;
  2. 经过相减exprParamType的类型来决定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

情形2:ParamType是通用引用类型(&&)

这种情形有点复杂,由于通用引用类型参数与右值引用参数的形式是同样的,可是它们是有区别的,前者容许左值传入。类型推断的规则以下:

  1. 若是expr是左值,TParamType都推导为左值引用,尽管其形式上是右值引用(此时仅把&&匹配符,一旦匹配是左值引用,那么&&能够忽略了)。
  2. 若是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&& 

因此,只要区分开左值与右值传入,上面的类型推断就清晰多了。

情形3:ParamType不是指针也不是引用类型

若是ParamType既不是引用类型,也不是指针类型,那就意味着函数的参数是传值了:

template <typename T> void f(T param); // 此时param是传值方式 

传值方式意味着param是传入对象的一个新副本,相应地,类型推断规则为:

  1. 若是expr类型是引用,那么其引用属性被忽略;
  2. 若是忽略了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)类型 // 尽管如此,实际使用时差异不大,用于回调函数时,通常不会去修改那个函数吧 

auto类型推断

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来讲,仍然有三种情形:

  1. 类型修饰符是一个指针或者引用,可是不是通用引用;
  2. 类型修饰符是一个通用引用;
  3. 类型修饰符不是指针,也不是引用。

下面是具体例子:

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++14auto还能够用于函数返回类型的推断以及泛型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关键字

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,lvalueprvalue。当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函数是专门处理通用引用类型参数的,基本上就是传入的参数是右值,转化的仍是右值引用,若是是左值,那么转化的是左值引用,具体能够参考这里

相关文章
相关标签/搜索