美文网首页
Item 1: 理解模板类型推导

Item 1: 理解模板类型推导

作者: EVANMORE | 来源:发表于2017-12-23 14:07 被阅读68次

    心血来朝的就想翻译一下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的特性都会忽略
    • 模板板类型推导过程中,数组或函数做微参数时会退化成指针,除非模板函数的参数是引用类型

    相关文章

      网友评论

          本文标题:Item 1: 理解模板类型推导

          本文链接:https://www.haomeiwen.com/subject/npmkrxtx.html