C++11 型别推导
成千上万的C++11程序员使用模板编程,大量的人在不知道细节的情况下,往模板函数传递参数时都得到了完全满意的答案,这只能说明C++11类型推导设计的非常成功,但并不意味着了解细节不重要。本文是我在学习C++11该部分内容时的总结笔记。
C++11中最简单的函数模板大概为:
template <typename T>
void f(ParamType param);
该函数的调用则形如:
f(expr);
模板在编译期起作用。在编译期,编译器会根据expr
完成对型别T
和ParamType
的推导。对于不同的ParamType
存在不同的推导规则。主要可以分为三种:
ParamType
是指针或者引用,但不是万能引用(T&&)。ParamType
是万能引用。ParamType
既非指针也非引用。
下面我们分别讨论。
1. ParamType
是指针或者引用,但不是万能引用
此时推导规则是这样的
- 如果
expr
具有引用型别,将其引用部分忽略。 - 然后,对
expr
的型别和ParamType
的型别进行模式匹配,推导出型别T
。
ParamType
是引用型别
template <typename T>
void f(T& param);
又声明了如下变量:
int x = 3;
const int cx = x;
const int& rx = x;
在各次调用中,编译器的型别推导情况如下:
f(x); //将T推导为int, ParamType推导为int&
f(cx); //将T推导为 const int, ParamType推导为 const int&
f(rx); //将T推导为 const int, ParamType推导为 const int&
注意,对第二、三个调用,我们发现参数中的const
在型别推导中被保留了下来,所以我们向持有T&
形参类型的模板函数传递const
对象是安全的。
对于第三个调用,即便rx
具有引用型别,但ParamType
依然被推导成了const int&
,而不是const int&&
,符合规则二。
ParamType
是指针型别
其实没什么不同,考虑如下定义:
template <typename T>
void f(T* param);
int x = 3;
const int* px = &x;
则调用函数f
的型别推导如下:
f(&x); //将T推导为int, ParamType推导为int*
f(px); //将T推导为const int, ParamType推导为const int*
2. ParamType
是万能引用T&&
此时推导规则是:
- 如果
expr
是个左值,T
和ParamType
都会被推到为左值引用。 - 如果
expr
是个右值,则按“常规”情况推导,即按前面说的ParamType
是引用而非万能引用推导。
如:
template<typename T>
void f(T&& param);
int x = 3;
const int cx = x;
const int& rx = x;
f(x); //x是左值,T的型别被推到为int&,ParamType被推导为int&。
f(cx); //x是左值,T的型别被推到为const int&,ParamType被推导为const int&。
f(rx); //x是左值,T的型别被推到为const int&,ParamType被推导为const int&。
f(3); //3是右值,T的型别被推到为int,ParamType被推导为int&&。
3. ParamType
既非指针也非引用
此时就是常说的值传递:
template<typename T>
void f(T param);
所谓值传递,就是无论传入的是什么,param
都只是它的一个副本,一个全新的对象。其推导规则如下:
- 如果
expr
具有引用型别,则忽略其引用部分。 - 忽略
expr
的常量性(const)和挥发性(volatile)。
所以:
int x = 3;
const int cx = x;
const int& rx = x;
f(x); //T的型别被推到为int,ParamType被推导为int。
f(cx); //T的型别被推到为int,ParamType被推导为int。
f(rx); //T的型别被推到为int,ParamType被推导为int。
注意,第二次和第三次调用说明,即便rx
cx
本身是const
类型,但param
仍然不具有常量性
,因为param
是一个完全独立于实参的副本。所以对持有T
类型形参的模板函数传递常量参数时,参数仍然可以在函数体内被修改。
特别注意:之前讲过,如果形参是const
的引用或者指针,expr
的常量行会在型别推导中加以保留。我们考虑:expr
是个指向const
对象的const
指针,那么会如何推到呢?
template<typename T>
void f(T param);
const char* const ptr = "hello world"; //ptr是一个指向const对象的const指针
f(ptr); //此时ParamType的型别被推导为const char*
在上述例子中,星号右侧的const
表明:ptr指向的地址不能被改变;星号左侧的const
表明:ptr指向地址的内容不能被改变,即该对象(在本例中是一个字符串)本身不能被修改。
按照我们的推导[规则1],ptr
本身的常量性被忽略,即在函数体内可以修改ptr
指向的内容,包括把ptr
置为nullptr
,但是ptr
指向对象的常量性得到保留。
重点总结
- 模板性别推导过程中,具有引用型别的实参会被当成非引用型别处理。
- 对于持有万能引用形参的模板函数,处理左值实参时会做特殊处理。
- 对值传递形式的形参进行推导时,若实参型别中有
const
或volatile
修饰词时,它们会被忽略。 - 在模板型别推导过程中,数组货函数型别的实参会退化成对应的指针,除非他们被用来初始化引用。
引用折叠
– 以下内容为2020年5月8日补充
今天看Scott大神的Effective Modern C++ 条款28讲引用折叠,发现跟之前这篇文章内容类似,并且更容易记,遂补充之。
首先,编译器不允许声明一个变量的型别为引用的引用,即形如
int x;
auto& &rx = x; //编译错误!
所以对于上述的型别推导
template <typename T>
void f(ParamType param);
如果T
和ParamType
都具有引用语意,编译器会对它们进行所谓的引用折叠。
折叠规则如下:
- 如果
T
和ParamType
中有任意一个为左值引用,则折叠后推导出的param
的型别为左值引用。 - 如果
T
和ParamType
都为右值引用,则折叠后推导出的param
的型别为右值引用。 - 如果
param
是一个左值但非引用型别,推导出的结果为左值引用。 - 如果
param
是一个右值,且ParamType
为右值引用,则推导出的结果不具有引用型别,即param
的类型为T
。 - 如果
param
是一个右值,且ParamType
为左值引用,编译错误!不允许将右值传递给一个接收左值引用的模板函数!
至此,终于对C++11型别推导有一个还算清晰的认识了!!