总结一下C++11中涉及到左值引用和右值引用的场景。
引用:别名,生命时必须初始化、通过引用修改变量值。
1 定义
左值引用
定义:对左值的引用。目的是避免内存拷贝,类似c中的指针,两个场景:函数传参、函数返回值。
意外:左值引用也可以引用右值,但是限定为const T&,这样右值就不能修改,如果必须修改,就只能使用右值引用,这也是右值引用发明的初衷之一。
右值引用
定义:对右值的引用。两个场景:实现移动语义、实现完美转发。右值引用如果想指向左值,也就是左值转化为右值,通过std::move。
引用虽然分为左值引用和右值引用,这是它的属性,但是这个变量是一个左值。
2 区分
左值引用
可以放在=左边,内存可寻址、具名。eg:变量名、返回左值引用的函数调用、前置自增自减。
右值引用
只能放在=右边,不可寻址、不具名。
两种类型: 纯右值:字面量、返回非引用类型的函数调用、后置自增自减、算数表达式、逻辑表达式、 比较表达式
将亡值:C++11新引入的右值引用(帮助实现移动语义)相关的值类型。触发移动构造和移动赋值构造。
将亡值:用来触发移动构造和移动赋值构造,进行资源转移,之后,调用析构函数销毁。主要的职责是实现资源转移。(资源:IO、FD、堆内存、等)
2.1 移动语义
- 移动语义是要解决对象赋值的问题,避免资源的重新分配,实现资源的转移。移动语义的实现,是通过触发移动构造和移动赋值运算符实现的。
- STL实现了移动构造接口。比如,将右值(可以是参数直接构造、或者左值的move,结果都是实参为右值)送给push_back方法,会直接移动资源。
- unique_ptr
- function
在C++11之前,通过已有对象给新对象赋值,使用的是深拷贝。但是在某些场景下,比如IO、FD、堆内存,或者unique_ptr,这种资源是对象独占的,不能二次拷贝,需要从旧的对象转移到新的对象中,这个时候就需要使用右值作为形参,创建移动构造函数或者移动赋值运算法,将旧对象的资源转移的新对象,就对象变为将亡值去析构。
看下拷贝构造和移动构造的使用
int main(int argc, char** argv ){
cout << "--- copy construction ---" <<endl;
A a;
A b(a);
cout << "--- move construction ---" <<endl;
A c;
A d(std::move(c));
return 0;
}
============== 输出 ==============
(base) ➜ test ./a
--- copy construction ---
A():p = 0x11e6067e0
A(const A&):p = 0x11e606810
--- move construction ---
A():p = 0x11e606840
A(A&&):p = 0x11e606840
~A() 1
~A() 2
~A() 1
~A() 1
2.2 完美转发
什么是完美转发?
函数模板,可以将自己的参数完美地转发给内部调用的其他函数。
完美指:不仅能准确地转发参数的值,还能保证被转发的参数的左右值属性不变。
如下:在函数模板中的t,不仅能把t的值转发给func函数调用,同时还能把t的左值右值属性传进来。如果t的属性是左值,调用第一个func,如果t的属性是右值,调用第二个func。
void func(int& t){
cout << "lvalue:"<< t <<endl;
}
void func(int&& t){
cout << "rvalue:"<< t <<endl;
}
template<typename T>
void revoke(T && t){
func(std::forward<T>(t));
}
怎么解决的完美转发?
1 借用了万能引用
上述代码的T&&就是万能引用,要么使用类型推到auto,要么使用模板参数。具体的类型+&&是右值引用。通过引用的方式接收左右属性的值。
void func(int& t){
cout << "lvalue:"<< t <<endl;
}
void func(int&& t){
cout << "rvalue:"<< t <<endl;
}
template<typename T>
void revoke(T && t){ // 接收。不管t的属性是左值引用还是右值引用,t本身始终是一个左值。
func(std::forward<T>(t));//转发
}
int main(int argc, char** argv )
{
int i = 10;
revoke(10);// 解析为右值
revoke(i);// 解析为 int& 左值引用
}
========= 输出 =========
(base) ➜ test ./a
rvalue:10
lvalue:10
万能是如何正确解析引用的属性的?
引用折叠:只处理接收参数的问题
参数是左值或左值引用,T& && (第一个是值的属性,后两个来自万能引用折叠) 转化为T&
参数是右值或右值引用,T&& &&(第一个是值的属性,后两个来自万能引用折叠)转化为T&&
本质:提取参数的属性,看是左值还是右值。
// 只有在模板推到中,才允许多个引用符号
revoke(static_cast<int & n>);// int& && t --> int& t
revoke(static_cast<int && n>);// int&& && t --> int&& t
std::forward<T>(v)
主要作用如下:
-
完美转发(Perfect Forwarding):
std::forward
主要用于在泛型代码中完美转发参数。当你在一个模板函数中接收参数并希望将它们传递给另一个函数时,你可以使用std::forward
来保持参数的值分类。这样可以确保参数被转发到下游函数时保持原始的左值或右值属性,从而避免额外的拷贝或移动操作。 -
保留值分类(Preserve Value Category):
std::forward
可以确保参数在传递过程中保持其原始的值分类,即左值保持为左值,右值保持为右值。这对于支持完美转发的函数模板至关重要。 -
针对1和2也可以从解引用的角度理解,将左(右)值引用的引用解除,得到左(右)值。
综上:main函数中,将左值或者右值传递给revoke,revoke形参是万能引用,通过引用折叠确保可以同时接收和保持实参的值属性,但是t本身实际是一个左值,如何提取其指向的值的左值和右值属性呢?通过forward来保留值属性,传递给下游的函数。。
参考测试代码:
#include <iostream>
using namespace std;
class A{
public:
A(){
p = new int[10];
cout << "A():p = " << p <<endl;
}
// 拷贝构造函数:深拷贝
A(const A& a){
this->p = new int[10];
memcpy(this->p, a.p, 10 * sizeof(int));
cout << "A(const A&):p = " << p <<endl;
}
~A(){
if(p != nullptr){
delete [] p;
p = nullptr;
cout << "~A() 1" <<endl;
}
else{
cout << "~A() 2" <<endl;
}
}
A(A&& a){
this->p = a.p;
a.p = nullptr;
cout << "A(A&&):p = " << p <<endl;
}
private:
int* p;
};
int main(int argc, char** argv )
{
cout << "--- copy construction ---" <<endl;
A a;
A b(a);
cout << "--- move construction ---" <<endl;
A c;
A d(std::move(c));
return 0;
}