c++模板类型推导分析

原来int *constconst int*是不一样的…嗯?什么叫做还有const int* const?

看了一些c++模板类型推导规则的文章,感觉比较乱,有些总结的推导规则看起来是对的实际上不完全对,而有些推导规则看起来离谱实际上是对的…所以重新总结梳理一下模板的类型推导规则,也方便以后再次复习。

顶层const与底层const

首先需要了解一个基本概念:顶层const和底层const,也就是引言中提到的int *constconst int*的区别:

  • 顶层const表示变量本身是常量,即变量的值不可被修改

    • 例如const int或者T* const的形式

      1
      2
      3
      4
      5
      6
      int a = 10;
      const int b = 20; // 顶层 const:b 是常量,值不可变

      int* const p1 = &a; // 顶层 const:p1 是常量指针,地址不可变(但可修改指向的值)
      p1 = &b; // ❌ 错误:p1 的地址不可修改
      *p1 = 30; // ✅ 允许:修改 p1 指向的值
  • 底层const表示变量指向或引用的对象是常量,即不能通过该变量修改其指向的对象的值。它作用于指针或引用所指向的目标,而非变量本身。

    • 例如const int*或者const T&的形式

      1
      2
      3
      4
      5
      6
      7
      int x = 10;
      const int* p2 = &x; // 底层 const:p2 是普通指针,但指向的值不可变
      p2 = &b; // ✅ 允许:修改 p2 的地址
      *p2 = 30; // ❌ 错误:不能修改 p2 指向的值

      const int& r = x; // 底层 const:r 是常量引用,不可通过 r 修改 x
      r = 40; // ❌ 错误

一般情况下,顶层const可以被忽略,而底层const必须匹配。顶层const关注变量自身,底层 const关注指向的对象。

模板类型推导规则

有了顶层const和底层const的概念之后,模板类型推导规则可以总结如下;这里需要注意的是,T的类型和ParamType的类型不一定相同,这里的推导规则主要指的是T的类型推导规则,当然也会提一嘴ParamType类型推导的结果:

  • 引用传递:去除expr的引用部分,同时保留其const属性

    • 这里需要注意的是,如果形参是const T&形式,那么T的推导结果会去除const。这条规则看起来有点离谱,因为如果不做单独区分,推导ParamType的类型时const const和const的结果是一样的,而且也符合通用规则。这里做个实验简单验证一下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      #include<iostream>
      #include <typeinfo>
      using namespace std;
      template <typename T>
      void func(const T& param)
      {
      if(is_const<typename remove_reference<T>::type>::value) cout<<"const int"<<endl;
      else cout<<"int"<<endl;
      }
      int main()
      {
      int x=27;
      const int cx=x;
      const int &rx=x;
      func(x);
      func(cx);
      func(rx);
      return 0;
      }
    • 最后的输出结果:

      1
      2
      3
      int
      int
      int

      看来这条规则确实存在,至少在g++编译器中是存在的。

      ps. 这里还有一个小坑,要先使用std::remove_reference去除变量的引用之后,再使用is_const去判断,不然在通用引用传递时会发现输出全为int好像const全丢掉了…实际是因为传递的是左值所以T被推为了左值引用,没有去除引用is_const判断错误,而按引用传递直接使用没有出现bug,会正确判断则是因为按引用传递的第一步就是去除expr的引用c++很神奇吧

  • 传递:忽略expr的引用和顶层const

    • 这里有个小小的例子,也就是引言中提到的const int* const ptr:其中int* const部分为顶层const,表示指针变量本身是常量,不可以修改指针让指针指向其他对象;前一部分const则为底层const出现在外层的是底层const,出现在内层的却是顶层const,表示指针指向的对象具有常量性。在进行类型推导的过程中,首先其顶层const会被忽略,TParamType会被推导为const int*,即顶层const在值传递的过程中被消除,ptr指向对象的常量性会被保留,但自身的常量性被忽略。
  • 通用引用(万能引用)传递:

    • 如果expr为左值,TParamType都会被推导为左值引用;

    • 如果expr为右值,T会被推导为普通类型(保留const,与按引用传递规则一致)

    • 这一点也是所谓万能引用的基础,也是rpc中实现参数完美转发的关键,实例代码如下所示:

      1
      2
      3
      4
      5
      template<typename F, typename... Args>
      void forwardFunc(F&& func, Args&&... args)
      {
      func(std::forward<Args>(args)...);
      }

      这里使用std::forward是为了解决值类别(左值或右值)问题,若不采用std::forward<Args>(args)...进行转发,即使Args&&... args作为forwardFunc(F&& func, Args&&... args)的参数被模板推导为右值引用类型,也会在forwardFunc(F&& func, Args&&... args)函数内变为左值(换句话说,就算args的类型被推导为右值引用类型,但args本身为左值,因为args仍可以在forwardFunc(F&& func, Args&&... args)内被重新赋值),而完美转发可以将其恢复为原本的值类别(原本是左值,完美转发后还是左值;原本是右值,完美转发后还是右值)。

  • 数组形参:如果参数是数组时, 按值传递时T会被推导为指向数组首元素的指针, 而不是数组本身;如果按引用传递时则会保留对应的数组大小的信息,T的类型会被推导成实际的数组类型。利用这个特点,能够创造出一个模板,使得在编译期就知道传入的数组的大小:

    1
    2
    3
    4
    template<typename T, std::size_t N>
    constexpr std::size_t arraySize(T (&)[N]) noexcept {
    return N;
    }
  • 函数形参:同理,在按值传递时,如果参数是函数类型,则函数类型也会退化成函数指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void someFunc(int , double);

    template<typename T>
    void f1(T param);

    template<typename T>
    void f2(T& param);

    f1(someFunc);//param被推导成函数指针,具体的型别是void(*)(int,double)
    f2(someFunc);//param被推导成函数引用,具体的型别是void(&)(int,double)

Reference