Item 1: 理解模板类型推导

心血来朝的就想翻译一下Effective Modern C++,非严谨翻译,大伙儿凑合着看吧。
记得刚接触C++是考上大学那会儿,为了更好的融入大学生活,特意在那个开学前的暑假骑了一小时的自行车到隔壁镇,风尘仆仆的进到当时镇上唯一的一家网吧。战战兢兢的打开电脑,学会了CS(Counter Strike_)。学校第一学期就开了C++,刚摸上电脑就接触这么高深莫测的语言真事有够胆战心惊的,这C++的课一开就是一年,结果一年下来,面向对象啥的听起来依旧如天书。
先来看看历史吧

  • C++98(哥当年学的)只有一种类型推导:函数模板
  • C++11修改了这个规则,添加了两个新的:auto以及decltype
  • C++14继续扩展了auto及decltype的应用语境

自打上C++14,这语言就越发的灵活了,有太多的场景会出现类型推导,理解类型推导是怎么进行的就变的尤为重要。
这一章节会解释一下模板类型推导是遵循怎么个规则,auto/decltype又怎么在这个基础上进行类型推导。除此之外,我们还会教你怎么诱骗编译器按你想要的结果去工作,是不是很牛!

Item 1: 理解模板类型推导

无数的码农们每天都在愉快的使用着模板类型推导(“理所应当嘛,你你编译器当然得知道我说的是什么类型了,是吧”),然而对于内部怎么工作就全然不必去关心。
如果你正好是这些愉快码农中的一员,那我这里又一个好消息还有一个坏消息(你要先听哪一个?)。好消息是模板的类型推导和auto的类型推导系出同宗,如果你之前用C++98的template用的很愉悦,那么等你切换到C++11以后,auto的类型推导似乎是一样一样的。坏消息是模板类型推导规则被应用在auto类型推导的场景中时,往往不如模板类型推导那么直观,所以有必要去真正理解一下类型推导的规则。

先来看一段伪代码吧,思考这样的一个函数模板

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

可以这样调用这个函数

  f(expr); // call f with some expression

编译过程中,编译器依据expr来推导两个类型,一个是T,另一个是ParamType. 这两个类型经常是不一样的,因为ParamType经常会有一个比如说const的修饰符。
来看个例子,如下的定义模板函数

  template<typename T>
  void f (const T& param); // param type is const T&

并且这样调用这个函数

  int x = 0;
  f(x); //call f with an int

T被推导成int,ParamType推导成int&
这个例子里面T的类型就是函数入参expr的类型,x是int,自然能推导出T的类型也是int。但是并不是所有的时候都这样,T的类型推导有时候不单单取决于函数入参expr的类型,它还依赖于ParamType的类型。有如下三种场景

  • ParamType是一个指针或者引用类型,但不是一个全局引用(全局引用在item24中有描述。这会儿你只要知道有这样一种引用,它区别于左值引用或右值引用)
  • ParamType是一个全局引用
  • ParamType既不是指针也不是引用

我们这里有三种类型推导的场景,都以如下的方式定义函数模板并调用

  template<typename T>
  void f (ParamType param);
  f(expr); // deduce T and ParamType from expr

Case 1: ParamType是一个引用或指针,但不是全局引用

最简单的场景是ParamType是一个引用或者指针类型,但不是全局引用。这个时候类型推导是这么工作的

  1. 如果函数参数expr的类型是引用,忽略参数的引用特性
  2. 通过匹配expr的类型,获取ParamType的类型进而确定T的类型
    例如,这是我们的函数模板
  template <typename T>
  void f(T& param); // param is a reference 

我们定义如下的变量

  int x = 27;             // x is an int
  const in cx = x;    // cx is an const int
  const int& rx = x; //rx is a reference to 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&

第二个和第三个函数调用中,由于cx和rx指定成const类型,推导出T是const int,从而产生了参数类型是const int&。 这一点对于函数调用者来说很重要。当传 递一个const对象给一个引用类型的参数,函数调用者期望这个对象维持const特性(不可以修改)。例如,期望这个函数参数是一个const引用。这就是为什么传递一个const对象给这样的模板函数(携带T&参数)是安全的,对象常量性也成为了类型T推导的一部分了。
第三个例子中,即使rx的类型是一个引用,推导出来T的类型依旧是一个非引用类型(non-reference)。这是由于在类型推导过程中,rx的引用性(reference-ness)被忽略了。
上述的这些例子都是左值引用类型,但是类型推导的规则对于右值引用参数同样有用。当然,只有右值实参能传递给右值引用参数,但是这个限制不会影响类型推导

如果我们修改了函数f的参数,从T&变成constT&,这时候发生了一点点改变。cx和rx的常量性依旧会得以保留。但是这个时候类型T就不会再有const特性了(不需要推导成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&

和之前一样,rx的引用性(reference-ness)在类型推导的过程中忽略了

如果参数是变成了指针(或是一个指向const的指针),类型推导依旧遵循同样的规则

  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*

开始打盹儿了吧,因为c++的类型推导规则看起来是那么的理所应当的,大家会很自然的认为类型推导不就应该是那样的么。但当我们真正的一条条罗列出来所以然的时候一下就变的好枯燥了。

Case 2: ParamType是一个全局引用

对于模板函数参数是全局引用的场景(T&&),类型推导就不是那么显而易见了。这些参数往往被声明称右值引用(例如,一个函数模板的入参类型T,一个全局引用的声明方法是T&&),当左值参数传递进来时,这两种函数模板的行为是不一样的。在Item24中会详细描述,这里概述一下

  • 如果expr是一个左值,T和Paramtype都被推导成为左值引用
  • 如果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 int&&

很明显当使用全局引用的时候,类型推导区分左值参数和右值参数。对于non-universal引用来说,这是从未有过的,Item 24会详细的解释这个原因。

Case 3: ParamType既不是指针也不是引用

这里我们来说说传值调用

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

这里param是传入值的一个拷贝,一个全新的对象。param是一个全新对象的事实驱动T的类型推导规则

  1. 和之前一样,如果expr是引用类型,忽略入参的引用特性(reference-ness)
  2. 而后如果expr是const类型,一并忽略。如果是volatile类型,继续忽略(volatile不常用,详细参看Item40)
    所以
  int x = 27;              // as before
  const int cx = x;    //  as before
  const int& rx = x;  //  as before
  
  f(x)          // T`s and param`s type are both int
  f(cx)        // T`s and param`s type are again both int 
  f(rx)        // T`s and param`s type are still both int

注意到这里cx和rx虽然代表const值,但param是全新的对象(cx或rx的一个拷贝),它不是const,这就说的通了。这就是为啥expr的这些特性(constness/volatileness/etc.)在类型推导的过程中都被忽略了

这里要记住只有传值参数的时候才会忽略这些const等等。但是当考虑这么个case,expr是一个指向const对象的const指针,然后expr按值传递进函数。如下,

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

  const char* const ptr = "Fun with pointers"  // ptr is const pointer to const   object

  f(ptr);  //  pass arg of the type const char* const

ptr这里是一个const指针,不能指向别的地方了,同样也不能设置成null。当ptr作为函数调用参数时,指针自身(ptr)会按值传递,指针(string的地址)复制到了param。ptr的常量性(constness)会被忽略掉,这时候param的类型推导出来是const char*,新的指针param可以指向不同的位置了,但是当前param指向的内容是不能改变的(这也很显而易见的)

数组作为参数

数组类型有别于指针类型,虽然它们有时候看起来可以互换。造成这种假象的原因是,很多场景下,数组会退化成指向数组头的指针。正因为有这种退化,使得下面代码能编译通过

  const char name[] = "J.P.Briggs"    //  name`s type is const char[13]
  
  char char * ptrToName = name;    //  arrary decays to pointer

这里的ptrToName被初始化成name,name是一个const类型的数组。

但是当传递一个数组给传值调用的模板函数的时候会发生些啥?参看下面的伪代码。

  template<typename T>
  void f(T param);    // template with by-value parameter
  
  f(name);      // what types a deduced for T and param?

发现了没,函数的参数好像并没有数组类型嘛!
我们来看一个看起来有点儿像数组类型作为入参的例子。下面的函数定义就是合法的。

  void myFunc(int param[]);

但是这里的参数param是被认作为一个指针的,意味着myFunc和下面定义的函数是等价的

  void myFunc(int* param);    // same function as above

正是有上述例子的存在,才使得数组和指针等价这个假象得以被很多人接受。
由于数组参数声明退化成指针参数,当数组作为一个值传递给一个模板函数,推导出来的类型应该是指针类型,意味着下面的代码中T被推导成const char*。

  f(name);    // name is array, but T deduced as const char*

接下来我们有一种曲线救国的方法(见证奇迹的时刻),虽然函数不能声明一个数组类型的参数,但是可以声明一个数组的引用类型参数。

  template<typename T>
  void f(T& param);    // template with by-reference parameter

然后传递一个数组给这个函数

  f(name);      // pass array to f

这个时候T的类型就真正的变成一个array。这个类型还隐含了数组的大小。这个例子里面f的参数类型是const char(&)[13].

有意思的是,声明一个指向数组的引用使得我们可以创建这样一个模板函数,这个模板可以推导一个数组包含的元素个数。

  // return size of an array as a compile-time constant. (The
  // array parameter has no name, because we care only about
  // the number of elements it contains.)
  template <typename T, std::size_t N>                       
  constexprstd::size_t arraySize(T (&) [N]) noexcept 
  {                          
      // see info below on constexpr and noexcept
      return N
  }

正如Item15中描述的,声明这样的函数constexpr,使得在编译过程中就能获得函数运行结果。所以下面的代码实现就变的可行了,我们可以定义一个新的数组,这个数组的大小和另一个数组一样。

  int keyVals[] = { 1, 3, 7, 9, 11, 22, 35};  //keyVals has 7 elements

  int mappedVals[arraySize(keyVals)];  // so does mappedVals

当然,你可能更加喜欢std::array来定义数组。

  std::array<int, arraySize(keyVals)> mappedVals;// mappedVals size is 7

函数作为参数

C++里面,函数同样也可以退化成函数指针,前面讨论的那些类型推导规则这里同样适用 。

  void someFunc(int, double);    // someFunc is a function;
                                                  // type is void(int, double)
  template<typename T>
  void f1(T param) ;      // in f1, param is passed by value

  template<typename T>
  void f1(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 a ref-to-func;
                             // type is void(&)(int, double)

到这儿你就知道这些模板类型推导的规则了,所以吧,这些规则看上去就是这么的简单直接。唯一的污点就是universal references场景下的左值参数,还有退化为指针的规则。那能不能更简单点儿,抓住编译器然后命令它“你都给我推导成啥啥类型?”,看看Item 4吧,你会找到答案的

记住以下几点

  • 模板类型推导时,忽略引用类型参数的引用性(reference-ness)
  • 给universal reference参数进行类型推导时,左值要特别对待
  • 传值参数的类型推导,入参的诸如所有const /volatile的特性都会忽略
  • 模板板类型推导过程中,数组或函数做微参数时会退化成指针,除非模板函数的参数是引用类型
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容