Modern C++核心内容(一)—— 函数模板和auto关键字的类型推导

目录

  • 一、函数模板中的类型推导
    • 1.1 ParamType是引用或者指针,但不是万能引用(Universal Reference)
    • 1.2 ParamType是万能引用(Universal Reference)
    • 1.3 ParamType既不是指针,也不是引用
  • 二、auto类型推导
  • 三、一些比较边角的情况

说到C++的类型推导,绝大部分的C++ programmer都不陌生,多多少少在编码的过程中都会用到,尤其是进入C++ 11之后,就算不用函数模板也免不了会用auto,绝大部时候,即便不了解内部的推导规则,往往也能得到预期的结果,但如果不了解内部机制有可能会犯一些非预期的错误,而且了解内部机制也有利于我们写出更优的代码,Modern C++(c++ 11之后版本的统称)包含了三种类型推导:原有的函数模板推导,以及新增的auto推导和decltype推导,在C++14之后进而增加了auto和decltype结合的使用方式,其中模板函数推导和auto推导很类似,这篇文章就来聊一聊这两种最常见的类型推导,decltype以后再讨论。本篇的内容主要是结合看过的Effective Modern C++、网上关于类型推导的内容以及自己的一些理解和思考,例子主要来自Effective Modern C++,这本书写得很不错,而且很实用,强烈推荐,中英文版我都看过,中文版翻译得还可以,但有些地方有笔误,感觉还是英文版看起来更顺。

一、函数模板中的类型推导

在C++ 11之前,只有在函数模板这个场景下才会有类型推导,这也是最经典的一种类型推导,可以让我们写一个模板同时支持各种类型,而且不需要手动指定类型,本质上实在编译过程中根据实际的调用情况生成所需要的函数,也就是所谓的实例化。以下是一个典型的函数模板的定义和使用:

//定义
template<typename T>
void f(ParamType param); 

//调用
f(expr); 

函数模板的类型推导里,涉及到以下三个部分:

  • T:模板参数,这个大家应该都不陌生,对于某些不使用推导的函数模板需要在尖括号里指定才能实例化。
  • ParamType:实例化后的函数形参类型。
  • expr:用来调用的表达式。

在编译过程实例化的时候,进行类型推导的依据就是expr,而推导的目标有两部分,一是T,二是ParamType,ParamType决定了生成的函数的实际形参类型,而T则是用来在函数内部进行相应类型的声明,同时在嵌套调用其他模板的时候也很有用,比如Modern C++中很重要的一个特性完美转发就需要用到T中隐含的信息。

对于函数模板中的类型推导,可以总结为以下三类:

  • ParamType是引用或者指针,但不是万能引用(Universal Reference)
  • ParamType是万能引用(Universal Reference)
  • ParamType既不是指针,也不是引用

1.1 ParamType是引用或者指针,但不是万能引用(Universal Reference)

这种类型也是我们最常见的一种类型,其中核心的推导规则如下:

  • 忽略expr的引用属性。
  • 把expr和ParamType进行模式匹配,进而确定T。

其中第一条规则很好理解,调用表达式是不是引用不影响推导结果,这和我们调用普通函数也一样,参数进入函数是传值还是传引用完全却决于函数形参而不是实参。而第二条所说的进行模式匹配则稍微灵活一些,本质上是综合考虑ParamType的额外限制和expr的额外限制,以下是一个ParamType为引用的典型例子

template<typename T>
void f(T& param);    // param is a reference and we have these variable declarations,

int x = 27;             // x is an int
const int cx = x;       // cx is a const int
const int& rx = x;      // rx is a reference to x as a const int

f(x);                   // T is int, param's type is int&
f(cx);                  // T is const int, param's type is const int&
f(rx);                  // T is const int, param's type is const int&

这里的ParamType是T&,也就是需要形参为引用,三个expr的类型分别是int,const int和const int&,也就是考虑以下三个模式匹配:

  1. int 和 T&,expr就是一个没有啥额外要求的int,ParamType的附加条件是引用,所以推出来的ParamType是 int&,要int& == T&,T自然是int。
  2. const int 和 T&,expr的附加条件是const,ParamType的附加条件是引用,所以推出来的ParamType是const int&,要const int& == T&,T自然是const int。
  3. 因为不考虑expr的引用属性,所以推导结果和2完全一样。

我们再来看一个ParamType是const T&的例子,和上面那个例子的区别就是ParamType多了个const:

template<typename T>
void f(const T& param);  // param is now a ref-to-const

int x = 27;              // as before
const int cx = x;        // as before
const int& rx = x;       // as before

f(x);                    // T is int, param's type is const int&
f(cx);                   // T is int, param's type is const int&
f(rx);                   // T is int, param's type is const int&

ParamType是const T&,也就是形参需要为const引用,三个expr的类型分别是int,const int和const int&,也就是以下三个模式匹配:

  1. int 和 const T&,expr是一个没有啥额外要求的int,ParamType的附加条件是const引用,所以推出来的ParamType是 const int&,要const int& == const T&,T自然是int。
  2. const int 和 const T&,expr的附加条件是const,ParamType的附加条件是const引用,合起来推出来的ParamType自然是const int&,const int& == const T&,T仍然是int。
  3. 因为不用考虑expr的引用属性,推导情况同2。

指针本质上和引用是一样的,所以推导也类似,如下:

template<typename T>
void f(T* param);        // param is now a pointer

int x = 27;              // as before
const int *px = &x;      // px is a ptr to x as a const int

f(&x);                   // T is int, param's type is int*
f(px);                   // T is const int, param's type is const int* 

这里ParamType是T*,也就是需要形参为指针,两个expr的类型分别是int*,和const int *,也就是以下两个模式匹配:

  1. int* 和 T*,expr是一个int*,而ParamType也是T*,都是要指针没其他附加条件,所以推出来的ParamType是 int*,要 int* == T*,T自然是int。
  2. const int* 和 T*,expr除了指针外的附加条件是const,ParamType只是T*,所以推出来的ParamType是const int*,要const int* == T*,T自然是const int。

一切都是那么的自然。

1.2 ParamType是万能引用(Universal Reference)

这种情况是相对特殊的一种,首先是ParamType必须为T&&这种形式,看上去像是右值的定义,但是在包含类型推导的函数模板里有着特殊的含义,也就是所谓的万能引用,完美转发就是通过万能引用来实现的,后面详细聊一聊,总的来说核心规则如下:

  • 如果expr是左值,T和ParamType都会被推导为左值引用。这也是整个函数模板类型推导里T唯一会被推导成引用的情况。
  • 如果expr是右值,推导方式参照第一种类型。下面我们就来看一个例子:
template<typename T>
void f(T&& param);       // param is now a universal reference

int x = 27;              // as before
const int cx = x;        // as before
const int& rx = x;       // as before

f(x);                    // x is lvalue, so T is int&, param's type is also int&
f(cx);                   // cx is lvalue, so T is const int&, param's type is also const int&
f(rx);                   // rx is lvalue, so T is const int&, param's type is also const int&
f(27);                   // 27 is rvalue, so T is int, param's type is therefore int&&

这种类型的ParamType的格式很固定,就是T&&,所以我们只需要考虑expr,对于x,cx,rx,27,类型分别为左值的int,const int,const int&和右值的int。这里我们仍然可以推导出ParamType的实际类型。
对于x,cx,rx,因为是左值,结合上面的规则,ParamType必须为左值引用,x就是个int,对于cx和rx,因为expr有没有引用没区别,所以实际都是多了一个const的限制,所以ParamType分别为int&, const int&和const int&,从而分别为T&& == int&, T&& == const int&和T&& == const,然后问题来了,T的类型是什么?答案是引用折叠,对于模板里的完美转发,是有引用折叠的,有兴趣的可以先自行了解下的,简单来说就是在这个场景里有以下的转换规则:
Modern C++核心内容(一)—— 函数模板和auto关键字的类型推导_第1张图片
具体到我们的例子就是& && == &,所以T 和ParamType的类型一样,分别也为int&, const int&和const int&,由于引用折叠在T是左值引用的时候T&&就等于T。

而对于27这个右值,T&&类型的ParamType扮演的就是一个右值引用,int类型的右值引用自然是int&&,所以T是int。

这里再多提一句,即便某个变量的类型是右值引用,包含这个变量名字的表达式也是左值,如果讲这种类型传入上面那个函数模板,仍然会当成左值来处理,如果想要被当成传入的是右值,通常会使用std::move。

1.3 ParamType既不是指针,也不是引用

这种情况也比较简单,ParamType既不是指针,也不是引用意味这是传值,所以核心就是expr的一系列const、引用啥的通通都可以不要,包括volatile也会被忽略。一个简单的例子如下:

template<typename T>
void f(T param);         // param is now passed by value

int x = 27;          // as before
const int cx = x;    // as before
const int& rx = x;   // as before

f(x);                // T's and param's types are both int
f(cx);               // T's and param's types are again both int
f(rx);               // T's and param's types are still both int 

ParamType是T,所以就是个没什么附加条件的传值,x cx 和rx最终都产出了int的推导结果,指针是一种特殊的值,也能传入这种模板,如果传入的是个int*,那么自然推导结果也是int*。

如果ParamType改为const T,如下:

template<typename T>
void f(const T param);         // param is now passed by value

int x = 27;          // as before
const int cx = x;    // as before
const int& rx = x;   // as before

f(x);                // T is int, param's type is const int
f(cx);               // T is int, param's type is const int
f(rx);               // T is int, param's type is const int

显然ParamType都会是const int,那么T都是int。

二、auto类型推导

auto也算是c++ 11之后引入的一个关键特性了,特别常用,本质上他和函数模板的类型推导几乎一致,auto相当于T,整个变量名前的部分相当于ParamType。

auto x = 27;    //类似ParamType为T
const auto cx = x;    //类似ParamType为const T
const auto& rx = x;    //类似ParamType为const T&

因此也完全适用前面说到的三种函数模板的推导类型。这里就不一一展开了,详细的说明可以直接去看下Effective Modern C++的Item 2.

三、一些比较边角的情况

上面介绍了函数模板和auto的类型推导的最通用的机制,掌握了这些基本上就能完全掌握各种相关类型推导的场景了,但实际上还有一些比较特殊比较少用的细分规则,这里简单概括下,主要有以下几种:

  1. 数组作为实参和函数作为实参:简单概括就是因为数组和函数都可以退化成指针,对于ParamType为传值的函数模板,如果将数组和函数传进去,实际上推导出来的类型会是数组包含元素类型的指针和函数指针,如果ParamType为引用,则能获得数组和函数的实际类型。
    函数模板推导的例子:
void someFunc(int, double);   // someFunc is a function,type is void(int, double)

template<typename T>
void f1(T param);    // in f1, param passed by value

template<typename T>
void f2(T& param);    // in f2, param passed by ref

f1(someFunc);    // param deduced as ptr-to-func,type is void (*)(int, double)
f2(someFunc);    // param deduced as ref-to-func,type is void (&)(int, double) 

auto推导的例子:

const char name[] = "R. N. Briggs";    // name's type is const char[13]  
auto arr1 = name;    // arr1's type is const char*
auto& arr2 = name;    // arr2's type is const char (&)[13]

void someFunc(int, double);    // someFunc is a function, type is void(int, double)
auto func1 = someFunc;         // func1's type is void (*)(int, double)
auto& func2 = someFunc;        // func2's type is void (&)(int, double)
  1. auto会认为用来调用的大括号括起来的元素是 std::initializer_list,而函数模板在推导的时候不会。如下:
auto x = { 11, 23, 9 };   // x's type is std::initializer_list

template<typename T>      // template with parameter declaration equivalent to x's declaration
void f(T param);             

f({ 11, 23, 9 });         // error! can't deduce type for T

详细的可以去看下Effective Modern C++的相关内容

【参考】
Effective Modern C++

你可能感兴趣的:(C++重点实用技术,c++,函数模板,auto关键字,类型推导)