右值引用、移动语义、完美转发
1 左值与右值
1.1 左值
特点 : 可以在等号左边,能够取地址,具名
例如:变量名 返回左值引用的函数调用 前置自增/减
赋值运算或复合赋值运算 解引用(*)等。
#include<iostream>
using namespace std;
//返回左值引用
int& fun1() {
int a = 1;
cout << "fun1" << endl;
return a;
}
int main() {
//左值可在等号左边,也可在等号右边
int a; //变量名
a = fun1(); //返回左值引用的函数调用
fun1() = 2;
++a = 2;//前置自增
--a = 2;//前置自减
int b = 2;
(a = b) = 2;//赋值运算
(a+=2)=1;//复合赋值运算
int* p = &a;
*p = 4; //解引用
return 0;
}
1.2 右值
特点:只能在等号右边,不能取地址,不具名
右值又分为:纯右值、将亡值
纯右值:字面值 ,返回非引用类型的函数调用,后置自增/减,算术表达式,逻辑表达式,比较表达式等。
#include<iostream>
using namespace std;
int& fun1() {
int a = 1;
cout << "fun1" << endl;
return a;
}
int fun2() {
cout << "fun2" << endl;
}
int main() {
//右值只能在等号右边,在左边编译器报错
int a = 100;//这里100为右值
100 = a; //不能做左值,报错
a = fun2();//返回非引用类型的函数调用
fun2() = 2;//不能做左值,报错
a++;
a++ = 2;//后置自增/减
a-- = 2;
(a + 2) = 1; //算术表达式
(a || 1) = 2; //逻辑表达式
(a < 2) = 2; //比较表达式
return 0;
}
将亡值:
- C++11 新引进的与右值引用(移动语义)相关的值类型,
- 将亡值用来触发移动构造函数或移动赋值构造,并进行资源转移,之后将调用析构函数
#include<iostream>
using namespace std;
class T
{
public:
T() {
cout << "T():" << this << endl;
}
~T() {
cout << "~T():" << this << endl;
}
T(const T&) {
cout << "(const T&)拷贝构造:" << this << endl;
}
void operator=(const T&) {
cout << "(operator=(const T&))拷贝赋值构造:" << this << endl;
}
T(T&&) {
cout << "T(T&&) 移动构造:" << this << endl;
}
void operator=(T&&) {
cout << "operator=(T&&)移动赋值构造:" << this << endl;
}
private:
int i;
};
T CreateT() {
T temp;
return temp;
}
int main() {
if (false) {//拷贝构造
T t1;
T t2 = t1;
T t3(t1);
T t4(t1);
}
if (false) {//拷贝赋值构造
T t1;
T t2;
t1 = t2;
}
if (false) {//移动构造
//需要禁用返回优化:-fno-elide-constructors
//1.看类有没有移动构造
//2.然后看类有没有拷贝函数
//3.报错
//调用移动构造
T t= CreateT(); //,如果去掉移动构造函数的话,调用的是拷贝构造函数
T t2(std::move(t)); //move() 将左值转为右值
T t3(CreateT());//这里CreateT()返回的是右值
}
if (true) {//移动赋值函数
T t;
t = T();
}
return 0;
}
注意:const左引用能指向右值,局限不能修改该值 T(const &T)
(1)执行CreateT();,发现调用一次普通构造和移动构造,很明显,在函数里调用普通构造,而返回是调用移动构造,此时函数CreateT里的temp就是将亡值,即调用该析构函数
(2)执行T t =CreateT(); 发现比上面多调用了一次移动构造,
为什么不是多调用一次拷贝构造函数呢?
因为CreateT()是一个右值,系统则优先调用移动构造函数
(3)move()将t 这个左值转化为右值,则优先调用移动构造函数
(4)与(2)相同
2 左值引用与右值引用
2.1 引用
- 做别名
- 声明时必须要初始化
- 通过引用修改修改变量值
2.2左值引用
2.21 定义及使用
左值引用是对左值的引用(左值引用是左值)
// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;
作用:左值引用用来避免对象拷贝,函数传参,函数返回值
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
使用场景:
// 1.左值引用做参数
void func1(string s)
{...}
void func2(const string& s)
{...}
int main()
{
string s1("Hello World!");
func1(s1); // 由于是传值传参且做的是深拷贝,代价较大
func2(s1); // 左值引用做参数减少了拷贝,提高了效率
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch) 传值返回存在拷贝且是深拷贝
// string& operator+=(char ch) 左值引用做返回值没有拷贝,提高了效率
s2 += '!';
return 0;
}
2.22 局限
左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。
当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。
string operator+(const string& s, char ch)
{
string ret(s);
ret.push_back(ch);
return ret;
}
// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题
于是,对于第二种情形,左值引用也无能为力,只能传值返回。
2.3 右值引用
2.31 定义
右值引用是对右值的引用(右值引用是左值)
右值引用的表示是在具体的变量类型名称后加两个 &,比如:
int&& rr = 4;
。右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;
。
注意:
右值引用引用右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。
比如:
double&& rr2 = x + y;
&rr2;
rr2 = 9.4;
//右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。
const double&& rr4 = x + y;
&rr4;
//可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。
了解右值引用的使用还得了解移动语义以及完美转发
2.4 实现移动语义
2.41 移动构造函数
在上面1.2右值一节中,其中的拷贝构造和移动构造
- 拷贝构造函数的参数是 const左值引用,接收左值或右值;
- 移动构造函数的参数是右值引用,只接收右值或被 move 的左值。
注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数,因为其可以少做一次深拷贝
2.42 STL应用
在STL使用加个std::move
会调用到移动语义函数,避免了深拷贝。
除非设计不允许移动,STL类大都支持移动语义函数,即可移动的
。 另外,编译器会默认在用户自定义的class
和struct
中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造
等函数。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move
触发移动语义,提升性能。
#include<iostream>
#include<list>
#include<string.h>
using namespace std;
class A{
public:
A() {
p= new int(10);
cout << "A():"<<p << endl;
}
~A() {
cout << "~A():" ;
if(p != nullptr){
delete[] p;
p = nullptr;
cout<<"~A1()"<<endl;
}
else{
cout<<"~A2()"<<endl;
}
}
A(const A& a) {
cout << "(const A&)拷贝构造:" <<endl;
p = new int(10);
memcpy(p,a.p,10*sizeof(int));
}
void operator=(const A& a) {
cout << "(operator=(const A&))拷贝赋值构造:" <<endl;
p = new int(10);
memcpy(p,a.p,10*sizeof(int));
}
A(A&& a) {
cout << "A(A&&) 移动构造:" << endl;
p = a.p;
a.p = nullptr;
}
void operator=(A&& a) {
cout << "operator=(T&&)移动赋值构造:" << endl;
p = a.p;
a.p = nullptr;
}
int *p;
};
void func(int && a ){
cout<<"rvalue = "<< a << endl;
}
int main(){
list<A> alist;
alist.push_back(A());//A()为匿名函数
auto &ele = alist.front();//获取头部元素
cout<<"ele.p:"<<ele.p<<endl;
return 0;
}
结果调用的是移动构造,而不是拷贝构造
2.43 unique_ptr智能指针
智能指针也实现了移动语义
2.5 完美转发
2.51 解释
- 函数模板可以将自己的参数完美地转发给内部调用的其他函数
- 完美是指不仅能准确地转发参数的值,还能保证被转发的参数的左右值属性不变
这里的remoke函数实现了完美转发。
2.52 万能引用
借用万能引用,通过引用的方式接收左右属性的值
引用折叠规则:
-
参数为左值或左值引用,T&&将转化为int &
-
参数为右值或右值引用,T&&将转化为int &&
系统推导的类型为万能引用,如 T&& t,auto &&t
template<typename T> void remoke(T&& t);
remoke(n);//相当于func(n)
此时T识别为int&,即
int& &&t ==> int &t;
remoke(100);//相当于func(100)
此时T识别为int&,即
int& &&t ==> int &t;
remoke(static_cast<int &> n);//强制转换为左值引用
此时T识别为int&,即
int& &&t ==> int &t;
remoke(static_cast<int &&> n);//强制转换为右值引用,并使其变成右值
此时T识别为int&&,即
int&& &&t ==> int &&t;
2.23 用forward<>()来实现透传
上面我们实现了传参,如果没有forward函数的话,无论我们传的是左值还是右值,调用的都是func(int & n),因为T&& t推导出来的t,只能是左值引用或者右值引用
左值引用与右值引用都是左值
#include<iostream>
using namespace std;
void func(int& n ){
cout<<"rvalue = "<< n << endl;
}
void func(int&& n){
cout <<"lvalue = "<< n <<endl;
}
template<typename T>
void remoke(T&& t){
func(t);
}
int main(){
int i=10;
remoke(100); //传一个右值
remoke(i); //传一个左值
return 0;
}
那我们如何来保证左右值属性不变呢?使用forward函数
修改remoke函数
template<typename T>
void remoke(T&& t){
func(forward<T>(t));
}
结果说明左右值属性传到了
这里我们再传左值引用和右值引用
int main(){
int i=10;
remoke(100);
remoke(i);
int a =11;
int &b =a;
int &&c = 111;
//static_cast<>()为类型强转
remoke(static_cast<int&>(b));//传入左值引用
remoke(static_cast<int&&>(c));//传入右值引用
return 0;
}
结果说明,传入左值引用或右值引用也成功了
2.24 问题
这里为什么要使用强转呢?如果你去掉强转,会发现传入参数c调用的是fun(int &)函数, 因为右值引用c是左值,所以T识别为int&
万能引用只是提供接收的左值和右值的能力,并将参数转化为对应的左右值引用。但是左值引用和右值引用作为参数时,c++后续使用都会被处理为左值, 因此调用左值模板函数。
因为 c 是一个左值,而static_cast<int&&>©是一个右值。
所以这里我严重怀疑
这与forward使用情形是差不多的,于是我想能不能让satic_cast来代替forward,结果居然是一样的
然后来看看forward的源码
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}
我们看到了他是个重载函数,通过万能引用和引用折叠,返还出我们的值,我们传入的值无非就是,带引用和不带引用两种,第二个它加了个断言用于不加引用的版本以防出错。我们看到了std::forward很简单,当传入参数为不是引用或为右值引用我就强转为右值引用,其他强转为左值引用。
但是你在传参时,还是避免不了要指定int&&引用类型,因为它不会因为他是右值引用类型而去强转成右值
2.25 static_cast<\T>()与forward<\T>()
在上面,我实在不明白static_cast俩种强转,于是我在函数里增加了自增
#include<iostream>
using namespace std;
void func(int& n ){
cout<<"lvalue = "<< n++ << endl;
}
void func(int&& n){
cout <<"rvalue = "<< n++ <<endl;
}
template<typename T>
void remoke(T&& t){
func(static_cast<T>(t));
}
int main(){
int a =11;
int &b =a;
int &&c = 111;
remoke(static_cast<int&>(b));//传入左值引用
remoke(static_cast<int&&>(b));//传入右值引用
remoke(b);
remoke(b);
cout<<"a="<< a <<endl;
return 0;
}
运行结果截图
这里我发现忘记把remoke函数里的static_case改回forward了,改回来再运行一下发现结果不同了:
这里static_cast使他失去了引用的功能?(修改a的值)
于是我多次执行下面这条语句,发现a修改了
func(static_cast<int&&>(b));
func(static_cast<int&&>(b));
func(static_cast<int&&>(b));
cout<<"a="<<a<<endl;
结果
rvalue = 11
rvalue = 12
rvalue = 13
a=14
那为什么上面不会修改?最后还是搞不懂,希望有人能帮我解惑
致谢
参考了很多(还有一些忘了在哪了),这里表示感谢
大厂面试讲解 C++11 面试题总结(左/右值引用、新特性、智能指针、类型推导、override,final关键字)_哔哩哔哩_bilibili
(115条消息) (转)一文读懂C++右值引用和std::move_右值引用的原理和本质_BBBourne的博客-CSDN博客
(115条消息) 详解 C++ 左值、右值、左值引用以及右值引用_c++ 引用 左值引用 右值引用_Hoshino373的博客-CSDN博客
End,如有更好的理解和思路请多多交流。