c++模板类型推导分析
原来int *const
和const int*
是不一样的…嗯?什么叫做还有const int* const
?
看了一些c++模板类型推导规则的文章,感觉比较乱,有些总结的推导规则看起来是对的实际上不完全对,而有些推导规则看起来离谱实际上是对的…所以重新总结梳理一下模板的类型推导规则,也方便以后再次复习。
顶层const与底层const
首先需要了解一个基本概念:顶层const和底层const,也就是引言中提到的int *const
和const int*
的区别:
顶层const表示变量本身是常量,即变量的值不可被修改
例如
const int
或者T* const
的形式1
2
3
4
5
6int 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
7int 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
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
3int
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会被忽略,T
和ParamType
会被推导为const int*
,即顶层const在值传递的过程中被消除,ptr
指向对象的常量性会被保留,但自身的常量性被忽略。
- 这里有个小小的例子,也就是引言中提到的
按通用引用(万能引用)传递:
如果
expr
为左值,T
和ParamType
都会被推导为左值引用;如果
expr
为右值,T
会被推导为普通类型(保留const,与按引用传递规则一致)这一点也是所谓万能引用的基础,也是
rpc
中实现参数完美转发的关键,实例代码如下所示:1
2
3
4
5template<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
4template<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
10void 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
- 【C++】详解模板类型推导
- 模板推导规则
- 《Modern Effective C++》读书笔记之条款一:理解函数模板类型推导规则
- 特别鸣谢:不出现服务器繁忙时的deepseek & Chatgpt
end☆~