C++ 引用折叠、万能引用与右值引用

从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&&代表一个右值引用。
  • 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值