20210221231021534.png

C++98仅有一套型别推导,用于函数模板。C++11对这套规则进行了一些改动并增加了两套规则,一套用于auto,一套用于decltype。后来C++14又扩展了能够运用auto和decltype的语境。


下面先重点讲一下auto关键字的使用语境:
请允许我使用一小段伪代码来说明。

函数模板的大致形如:

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

而一次调用如下:

f(expr);

在编译期,编译器将会通过expr推导推导T及ParamType的型别,这两个往往不一样。因为ParamType常会包含一些修饰词,例如const或者引用之类的。
下面将分三种情况进行分析:

情况一:ParamType是一个指针或者引用,但不是万能指针或引用(两个引用符号)

此时,型别推导会进行如下操作:

  • 若expr是具有引用型别,则先将引用部分忽略

  • 然后对expr的型别和ParamType进行型别匹配,进而决定T的型别。
     

结合以上两句并参考以下代码便可以理解:

    template<class 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& 类型
     
    // 在各个调用时,param及T的推导如下:
    f(x);  //T是int,param 为 int&
    f(cx); //T是const int,param为 const int&
    f(rx); //T是const int,param为 const int&

情况二:ParamType是一个万能引用

万能引用的声明型别写作T&&,其会根据实参类型型别,进行不同的处理:

  • 如果expr是左值,T和ParamType都会被推导为左值引用,这是T被推导为引用的唯一情况

  • 如果expr是右值,则按照常规情况进行处理(情况一)


结合以上两句并参考以下代码便可以理解:

    template<class 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& 类型
     
    // 在各个调用时,param及T的推导如下:
    f(x);  //x是左值,所以T是int&,param 也为 int&
    f(cx); //cx是左值,所以T是const int&,param也为 const int&
    f(rx); //x是左值,所以T是const int&,param为 const int&
    f(27); //x是右值,所以T是int,param为int&&

情况三:Paramtype即非指针也非引用,按值传递

如果是按值传递,则param相当于一个副本。

  • 若expr具有引用型别,则先忽略其引用部分

  • 若expr是个const、volatile对象,则也忽略其const、volatile关键字

结合以上两句并参考以下代码便可以理解:

    template<class 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& 类型
     
    在各个调用时,param及T的推导如下:
    f(x);  //T是int,param 也为 int
    f(cx); //T是int,param 也为 int
    f(rx); //T是int,param 也为 int

这里需要说明一下,虽然cx或者rx是const类型,即不可修改的,虽然此时在f函数中被去掉了const修饰词,但仍然是正确的,因为此时f()的实参是形参的一份拷贝,修改形参并不会改变实参,满足const属性要求

注意以下是难点,初学者可以不必了解:
除了以上三种情况外,还有一种情况,就是指针退化的情况,包括数组退化为数组指针,函数退化为函数指针。
当我们把数组传入到某个数组指针形参时,是可以可以编译通过的,因为数组在传递时,会被退化为数组指针。但是如果此时我们把ParamType是一个引用,再向其传入一个数组,此时数组将不会发生退化,及形参就是一个数组的引用。
例如:

    template <class T> 
    std::size_t arraySize(T& param){ //param为const int(&)[9]
        return 0;
    }
     
    int main()
    {
        int key[] = { 1,2,3,4,5,6,7,8,9 };
        arraySize(key);   //此时key被推导为 const int(&)[9]
        return 0;
    }

我们也可以借助这个特性来计算该数组的大小,代码如下:

template <class T,std::size_t N> 
std::size_t arraySize(T(&)[N]){ //此时入参是const int(&)[9],按照模式匹配规则,此时N为9
      cout << N << endl;
    return N;
}
int main()
{
    int key[] = { 1,2,3,4,5,6,7,8,9 };
    arraySize(key);   //此时key被推导为 const int(&)[9]
    return 0;
}

同理,函数也是一样,具体逻辑见代码:

    int someFunc(int,double);//someFunc是一个函数,类型是:void(int,double)
     
    template <class T>
    void f1(T param);   //按值传递
     
     
    template <class T>
    void f2(T& param); //按引用传递
     
     
    f1(someFunc); //此时param被推导为函数指针:void(*)(int,double)
     
    f2(someFunc); //此时param被推导为函数引用:void(&)(int,double)