0%

模板类型推导

模板类型推导

写了这么久C++都没思考过模板推导的相关内容,这次学习Effective Modern C++记录一下
已知模板函数:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr); //使用表达式调用f

在编译期间,编译器使用expr进行两个类型推导:一个是针对T的,另一个是针对ParamType的。这两个类型通常是不同的,因为ParamType包含一些修饰,比如const和引用修饰符
如:
1
2
template<typename T>
void f(const T& param); //ParamType是const T&

所以根据ParamType的类型存在三种情况:

ParamType是指针或者引用(非通用引用)

规则:

  1. 如果expr的类型是一个引用,忽略引用部分
  2. 然后expr的类型与ParamType进行模式匹配来决定T

    引用类型

    1
    2
    3
    template<typename T>
    void f(T& param); //param是一个引用
    f(expr);

    在这里ParamTypeT&也就是引用类型,并且param一定是个引用,但是得看是const &还是非const &

    1
    2
    3
    4
    5
    6
    7
    int x=27;                       //x是int
    const int cx=x; //cx是const int
    const int& rx=x; //rx是指向作为const int的x的引用

    f(x); //T是int,param的类型是int&
    f(cx); //T是const int,param的类型是const int&
    f(rx); //T是const int,param的类型是const int&
  3. int x 和 f(x)推导出T是int,param的类型是int&。这个很好理解,因为本来就是T&,生成的模板函数肯定也是带引用的,int就推出int

  4. const int cx 和 f(x)推导出T是const int,param的类型是int&。这个也好理解和合理,因为用户向f传入const对象时就是想对象保持不可改变性,编译器自然也要满足这种需求,所以也带const
  5. const int& rx 和 f(x)推导出T是const int,param的类型是const int&。这个符合第一点要求,引用部分被忽略,忽略后结果和2相同

测试:
在vs2022上写代码使用boost库里面的type_id_with_cvr进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <boost/type_index.hpp>
#include <iostream>
using namespace std;

//函数模板
template<typename T>
void f(T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;

//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';

//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}


int main()
{
int x = 27; //x是int
const int cx = x; //cx是const int
const int& rx = x; //rx是指向作为const int的x的引用

f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
}

1723219601030.png
此外,如果ParamType是const T&时,情况会有一些不同,但是情况和上面类似,不细讲
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param); //param现在是reference-to-const

int x = 27; //如之前一样
const int cx = x; //如之前一样
const int& rx = x; //如之前一样

f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&

指针类型

引用是另类的指针,理解上面引用后,指针也好理解,只要注意const会保持就行

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param); //param现在是指针

int x = 27; //同之前一样
const int *px = &x; //px是指向作为const int的x的指针

f(&x); //T是int,param的类型是int*
f(px); //T是const int,param的类型是const int*

vs2022 上运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <boost/type_index.hpp>
#include <iostream>
using namespace std;

//函数模板
template<typename T>
void f(T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;

//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';

//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}


int main()
{
int x = 27; //x是int
const int cx = x; //cx是const int
const int& rx = x; //rx是指向作为const int的x的引用

f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
}

1723220710506.png

ParamType是通用引用

通用引用是universal Reference翻译过来的,也有叫万能引用的,形式上和右值引用一样(&&),第一次接触这玩意还是实习的时候师兄跟我讲的,后面就回去了解了一下。这里默认大伙已经了解左值右值哈。
规则:

  1. 如果expr是左值,TParamType都会被推导为左值引用。需要注意的是右值引用也是左值(T唯一一种被推导为引用的情况)
  2. 如果expr是右值,就有点类似上面的内容(右值版推导)

    1. 如果expr的类型是一个引用,忽略引用部分
    2. 然后expr的类型与ParamType进行模式匹配来决定T
      1
      2
      3
      template<typename T>
      void f(T&& param); //param现在是一个通用引用类型
      f(expr)
      这里ParamType是T&& ,param最终一定是个引用,但是得看是左值引用& 还是右值&&
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      int x=27;                       //如之前一样
      const int cx=x; //如之前一样
      const int & rx=cx; //如之前一样

      f(x); //x是左值,所以T是int&,
      //param类型也是int&

      f(cx); //cx是左值,所以T是const int&,
      //param类型也是const int&

      f(rx); //rx是左值,所以T是const int&,
      //param类型也是const int&

      f(std::move(x)); // 右值引用也是左值,所以T是const int&,
      //param类型也是int&

      f(27); //27是右值,所以T是int,
      //param类型就是int&&
  3. int x 和 f(x)推导出T是int &,param的类型是int&。模板函数应该是这样void f(int& && param),,然后通过引用折叠变成void(int& param)

  4. const int cx 和 f(x)推导出T是const int&,param的类型是int&。同上,不过加了const。
  5. const int& rx 和 f(x)推导出T是const int&,param的类型是const int&。同上
  6. 27 和 f(27)推导出T是int,param的类型是int&&。模板函数是这样void(int&& param),使得f可以接受右值进行传入

说实话这个通用引用还挺奇妙,最终f可以接受所有值进行传入。
代码和运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <boost/type_index.hpp>
#include <iostream>
#include <utility>
using namespace std;

//函数模板
template<typename T>
void f(T&& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;

//显示T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';

//显示param类型
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}


int main()
{
int x = 27; //如之前一样
const int cx = x; //如之前一样
const int& rx = cx; //如之前一样

f(x); //x是左值,所以T是int&,
//param类型也是int&

f(cx); //cx是左值,所以T是const int&,
//param类型也是const int&

f(rx); //rx是左值,所以T是const int&,
//param类型也是const int&

f(std::move(x)); // 右值引用也是左值,所以T是const int&,
//param类型也是int&

f(27); //27是右值,所以T是int,
//param类型就是int&&
}

1723258751479.png

ParamType既不是指针也不是引用

大白话值传递

1
2
3
template<typename T>
void f(T param); //以传值的方式处理param
f(expr)

规则:

  1. 和之前一样,如果expr的类型是一个引用,忽略这个引用部分
  2. 如果忽略expr的引用性(reference-ness)之后,expr是一个const,那就再忽略const。如果它是volatile,也忽略volatile
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int x = 27;                       //如之前一样
    const int cx = x; //如之前一样
    const int& rx = cx; //如之前一样
    const int* px = &x; //px是指向作为const int的x的指针
    int* const px1 = &x;
    const int* const px2 = &x;

    f(x); //T和param的类型都是int
    f(cx); //T和param的类型都是int
    f(&x); //T和param的类型都是int*
    f(px); //T和param的类型都是cosnt int*
    f(px1); //T和param的类型都是 int*
    f(px2); //T和param的类型都是cosnt int*
    指针可能有些不一样,const 指针传进去,推导出来还是const ,但是 *const传进去const特性会被忽略,也就是说指针是可以更改指向的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #include <boost/type_index.hpp>
    #include <iostream>
    #include <utility>
    using namespace std;

    //函数模板
    template<typename T>
    void f(T param)
    {
    using std::cout;
    using boost::typeindex::type_id_with_cvr;

    //显示T
    cout << "T = "
    << type_id_with_cvr<T>().pretty_name()
    << '\n';

    //显示param类型
    cout << "param = "
    << type_id_with_cvr<decltype(param)>().pretty_name()
    << '\n';
    }


    int main()
    {
    int x = 27; //如之前一样
    const int cx = x; //如之前一样
    const int& rx = cx; //如之前一样
    const int* px = &x; //px是指向作为const int的x的指针
    int* const px1 = &x;
    const int* const px2 = &x;

    f(x); //T和param的类型都是int
    f(cx); //T和param的类型都是int
    f(&x); //T和param的类型都是int*
    f(px); //T和param的类型都是cosnt int*
    f(px1); //T和param的类型都是 int*
    f(px2); //T和param的类型都是cosnt int*
    }
    1723264877921.png

    其他

    存在一些特列,记录一下

    数组实参

    一般来说,数组传参时是退化为指针,推导的时候也应该影响不大
    1
    2
    3
    4
    5
    6
    7
    template<typename T>
    void f(T param); //以传值的方式处理param

    const char name[] = "J. P. Briggs"; //name的类型是const char[13]
    const char* ptrToName = name; //数组退化为指针
    f(name); //name是一个数组,但是T被推导为const char*
    f(ptrToName);
    1723267302280.png
    但是在ParamType作为引用时,就可以有指向数组的引用了,并且可以获得数组的大小
    1
    2
    3
    4
    template<typename T>
    void f(T& param); //传引用形参的模板
    const char name[] = "J. P. Briggs"; //name的类型是const char[13]
    f(name); //name是一个数组,但是T被推导为const char (&)[13]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include <boost/type_index.hpp>
    #include <iostream>
    #include <utility>
    using namespace std;

    //函数模板
    template<typename T>
    void f(T& param)
    {
    using std::cout;
    using boost::typeindex::type_id_with_cvr;

    //显示T
    cout << "T = "
    << type_id_with_cvr<T>().pretty_name()
    << '\n';

    //显示param类型
    cout << "param = "
    << type_id_with_cvr<decltype(param)>().pretty_name()
    << '\n';
    }


    int main()
    {
    const char name[] = "J. P. Briggs"; //name的类型是const char[13]
    f(name);
    }
    1723297363366.png
    应用:
    编译器就可以获得数组大小,不过sizeof好像也可以
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //在编译期间返回一个数组大小的常量值(//数组形参没有名字,
    //因为我们只关心数组的大小)
    template<typename T, std::size_t N> //关于
    constexpr std::size_t arraySize(T (&)[N]) noexcept //constexpr
    { //和noexcept
    return N; //的信息
    } //请看下面

    int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; //keyVals有七个元素

    int mappedVals[arraySize(keyVals)]; //mappedVals也有七个

    函数实参

    函数和数组类似,都会退化为指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void someFunc(int, double);         //someFunc是一个函数,
    //类型是void(int, double)

    template<typename T>
    void f1(T param); //传值给f1

    template<typename T>
    void f2(T & param); //传引用给f2

    f1(someFunc); //param被推导为指向函数的指针,
    //类型是void(*)(int, double)
    f2(someFunc); //param被推导为指向函数的引用,
    //类型是void(&)(int, double)