从C++11开始,新加入了右值
的概念,这里先简单介绍一下左值、右值及其引用的概念。
- 左值:非临时的(具名的、可以被取地址)。可以出现在 = 号左边或右边。
- 右值:临时的、不可以被取地址的,只能出现在 = 号的右边。
- 左值引用:对左值的引用就是左值引用,形式为
int& lref
。 - 右值引用:对右值的引用就是右值引用, 形式为
int&& rref
。
[!NOTE]
注:常量左值引用 是“万能”的引用类型,可以绑定到所有类型的值,包括非常量左值、常量左值、非常量右值和常量右值。
通常来讲右值是临时对象,声明周期短暂,一般在执行完当前这条表达式之后就被释放了。
通过将其赋值给右值引用,可以对其 “续命”,使其生命周期与右值引用类型变量的生命周期一样长。
由于C++11对右值、右值引用等概念的引入以及原来就存在的左值和左值引用等概念,延伸出了 引用折叠、通用引用(万能引用)等概念或者说特性。
引用折叠
我们把 引用折叠 拆解为 引用和 折叠 两个短语来解释。
引用:如文章开头部分所讲,是某个对象的别名,可以绑定左值或者右值。
折叠:所谓折叠意思是 C++ 不允许出现引用的引用
。而在一些场景下,比如函数模板参数中,参数推导完之后,可能会出现引用的引用,这个时候引用折叠规则会 将多个引用折叠为单个引用
。
引用折叠具体规则如下:只要其中一个是左值引用结果就是左值引用,否则才是右值引用 。
不同组合情况 | 声明类型 | 折叠类型 |
---|---|---|
引用的引用 | & & | & |
右值引用的引用 | && & | & |
引用的右值引用 | & && | & |
右值引用的右值引用 | && && | && |
万能引用
万能引用(通用引用)是在 参数推导中
(模板参数推导(T&&) 或者 auto&&推导),利用引用折叠的相关规则,使其既可以绑定左值引用,又可以接收右值引用,并且保持左右值const属性的引用类型,形如 T&& 或者 auto&&
。
万能引用的两个关键点👀:
- 必须涉及参数类型推导(T&& 或者 auto&&)
- 必须严格形如 T&& 或者 auto&& , 比如如果函数模板参数是 const T&&,就不是万能引用。
万能引用
允许等号右侧是任意合法的表达式,而等号左侧总是可以根据表达式类别,推演出合适的引用类型。
template <typename T>
void MyFunc(T&& value) {
}
void main() {
int a = 10;
const int b = 100;
MyFunc(a); // T 为int& 发生引用折叠:int& && ---> int&
MyFunc(b); // T 为const int& 发生引用折叠:constt int& && ---> const int&
MyFunc(100); // T 为int,不发生引用折叠
MyFunc(static_cast<const int&&>(100); // T 为 const int,不发生引用折叠
}
实际上,代码中四次函数模板调用实例化的模板函数分别如下所示:
template<>
void MyFunc<int &>(int & value) {
}
template<>
void MyFunc<const int &>(const int & value) {
}
template<>
void MyFunc<int>(int && value) {
}
template<>
void MyFunc<const int>(const int && value) {
}
区分万能引用与右值引用
从形式上看,万能引用的语法格式与右值引用很相似, 都是类型名后面跟着 &&。
当 && 出现在代码中时,并不一定是右值引用:
void f(Widget&& param); //右值引用
Widget&& var1 = Widget(); //右值引用
auto&& var2 = var1; //不是右值引用
template<typename T>
void func(std::vector<T>&& param); //右值引用
template<typename T>
void func(T&& param); //不是右值引用
对一个万能引用而言,类型推导是必要的;除此之外,引用声明的形式必须正确,并且该形式是被限制的。
1)它必须严格匹配 “T&&
” 形式 :
template <typename T>
void func(std::vector<T>&& param); // param是一个右值引用
当函数func
被调用的时候,类型T
会被推导(除非调用者显式地指定它,这种边缘情况我们不考虑)。
但是param
的类型声明并不是 T&&
⚠️ ,而是std::vector<T>&&
,是std::vector<T>&&
,是std::vector<T>&&
, 所以这里不是一个 万能引用,而是右值引用,因此只能绑定右值。 如果调用 函数 func 时传递了左值,编译器将会报错。
2)严格匹配 T&&
还要求不能出现多余的CV限定符,比如:
template <typename T>
void func(const T&& param); // param是一个右值引用
这里也会导致 param 失去万能引用的资格,变成右值引用。
3)此外,对于一个类模板内部的成员函数来讲,相比普通的函数模板,又有一些特殊的情况。
一个模板里面的一个成员函数形参类型严格匹配 “T&&
” 形式,未必是万能引用(通用引用), 因为类模板实例化的时候,其内部的成员函数未必发生类型推导。
template<class T, class Allocator = allocator<T>> //来自C++标准
class vector
{
public:
void push_back(T&& x);
// ...
}
push_back
函数的形参当然有一个通用引用的正确形式 T&&,然而,在这里并没有发生类型推导
。因为push_back
在有一个特定的vector
实例之前不可能存在,而实例化vector
时的类型已经决定了push_back
的声明。也就是说,
std::vector<Widget> v;
将会导致std::vector
模板被实例化为以下代码:
class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); //右值引用
…
};
可以清楚地看到,函数push_back
不包含任何类型推导。
总结
- 如果一个函数模板形参的类型为
T&&
,并且T
需要被推导得知,或者如果一个对象被声明为auto&&
,这个形参或者对象就是一个通用引用。 - 如果类型声明的形式不是标准的
type&&
,或者如果类型推导没有发生,那么type&&
代表一个右值引用。 - 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。