lee-romantic 's Blog
Everything is OK!
Toggle navigation
lee-romantic 's Blog
主页
About Me
归档
标签
C++ 左值右值、移动语义、完美转发
2022-12-02 15:43:57
49
0
0
lee-romantic
# 1、左值右值 ## 1.1 左值 - 放在等式左边,可以取地址并且有名字的变量。比如: - 函数名和变量名 - 返回左值引用的函数调用 - 前置自增减表达式++i、--i - 由赋值表达式或赋值运算符连接的表达式(a=b,a+=b等) - 解引用表达式*p - 字符串字面值"abcd" ``` int a= 4; //a是左值,4 作为普通字面量是右值 ``` ## 1.2 右值 - 不能取地址的没有名字的东西就是右值,一般放在等式右边。C++11以后,右值又分为纯右值和将亡值。 ## 1.2.1 纯右值 - 运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。例如: - 除字符串字面值外的字面值 - 返回非引用类型的函数调用 - 后置自增减表达式i++、i-- - 算术表达式(a+b,a*b, a&&b,a==b等) - 取地址表达式等(&a) ## 1.2.2 将亡值 - 将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值。将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间的方式获取的值,在确保其它变量不再被使用或即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。 ``` 1 class A { 2 xxx; 3 }; 4 A a; 5 auto c = std::move(a); // std::move(a)是将亡值 6 auto d = static_cast<A&&>(a); // static_cast<A&&>(a)是将亡值 ``` # 3、左(右)值引用 左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型。都是别名,必须立即初始化。 ``` type &name = exp; // 左值引用 type &&name = exp; // 右值引用 ``` std::move函数强制把左值转换为右值。 ``` int a = 4; int &&b = a; // error, a是左值 int &&c = std::move(a); // ok ``` # 4、深(浅)拷贝 ## 4.1 浅拷贝 浅拷贝只是数据的简单赋值,不会另外开辟空间。比如下面的代码,`a`和`b`的`data_`指针指向来同一块内存,两个输出的是相同的地址,这就是浅拷贝,那再析构时data_内存会被释放两次,导致程序出问题。 ``` class A { public: A(int size) : size_(size) { data_ = new int[size]; } A(){} A(const A& a) { size_ = a.size_; data_ = a.data_; cout << "copy " << endl; } ~A() { delete[] data_; } int *data_; int size_; }; int main() { A a(10); A b = a; cout << "b " << b.data_ << endl; cout << "a " << a.data_ << endl; return 0; } ``` 输出 ``` copy b 0x100750b60 a 0x100750b60 CPP(893,0x1000ffd40) malloc: *** error for object 0x100750b60: pointer being freed was not allocated CPP(893,0x1000ffd40) malloc: *** set a breakpoint in malloc_error_break to debug ``` ## 4.1 深拷贝 深拷贝就是在拷贝对象时,如果被拷贝的内部还有指针指向其它资源,自己需要重新开辟一块新内存资源,而不是简单的赋值。比如,就需要修改上述Class A中的拷贝构造函数和析构函数为: ``` A(const A& a){ size_ = a.size_; data_ = new int[size_]; memcpy(data_, a.data_,size_); std::cout<<"copy"<<std::endl; } ~A(){ delete [] data_; } ``` 输出: ``` copy b 0x1051c65d0 a 0x1051c65a0 ``` # 5、移动语义与完美转发 ## 5.1、移动语义 可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存来复制对方资源,而对于移动语义,类似于转让或者资源窃取的意思,将那块资源转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢? 答案是通过移动构造函数。 (注意,在C++11移动构造函数出来前,我们没办法在接口层面区分是应该要深拷贝还是浅拷贝,通常在拷贝构造函数中是需要深拷贝的,自然速度慢) 移动构造函数参数A&&需要的是右值,std::move(a)就是将其变成右值(std::move(a)其源码本质就是``static_cast<A&&>(a)``而已),从而可以使用移动构造函数,内部直接复制指针而避免拷贝,加快速度,但是被移动后的对象,哪怕还可以正常使用,也不应该再使用: ``` class A{ public: A(int size):size_(size){ data_ = new int[size]; } A(){} A(const A& a){ size_ = a.size_; data_ = new int[size_]; memcpy(data_, a.data_,size_); std::cout<<"copy"<<std::endl; } A(A&& a){ this->data_ = a.data_; a.data_=nullptr; // 被"窃取"的对象,不应该再使用 std::cout<<"move "<<endl; } ~A(){ if(data_ != nullptr) { delete [] data_; } } int *data_; int size_; }; int main(int argc, const char * argv[]) { // insert code here... A a(10); A b = a; std::cout<<"a "<<a.data_<<std::endl; A c=std::move(a);//调用移动构造函数,而不是赋值函数, 因为这是在构造,而不是对一个已经构造完成了的对象赋值,等价于A c(std::move(a)); std::cout<<"b "<<b.data_<<std::endl; std::cout<<"c "<<c.data_<<std::endl; std::cout<<"a "<<a.data_<<std::endl; return 0; } ``` ``` copy a 0x1007c2630 move b 0x1007c2660 c 0x1007c2630 a 0x0 ``` 以上例子也很容易说明,对于普通数据比如int,使用std::move进行移动,速度不一定比拷贝快,这取决于你要如何使用move后的右值,毕竟std::move本质上什么都不做。 ## 5.2、完美转发 - 完美转发指可以写一个接受任意实参的函数模版,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实惨是右值那目标函数实参是右值那目标函数的实参也是右值。如何实现完美转发呢,答案是使用std::forward()。 ``` void PrintV(int &t){ std::cout<<"lvalue"<<std::endl; } void PrintV(int &&t){ std::cout<<"rvalue"<<std::endl; } template<typename T> void Test(T &&t){ PrintV(t); PrintV(std::forward<T>(t)); PrintV(std::move(t)); } int main(int argc, const char * argv[]) { // insert code here... Test(1); // lvalue rvalue rvalue int a = 1; Test(a); // lvalue lvalue rvalue Test(std::forward<int>(a)); // lvalue rvalue rvalue Test(std::forward<int&>(a)); // lvalue lvalue rvalue Test(std::forward<int&&>(a)); // lvalue rvalue rvalue return 0; } ``` ``` lvalue rvalue rvalue lvalue lvalue rvalue lvalue rvalue rvalue lvalue lvalue rvalue lvalue rvalue rvalue ``` - 引用折叠规则: - 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&) - 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&) - [参考](https://blog.csdn.net/TABE_/article/details/122609775) - 根据引用折叠,上面的代码中: - `Test(1)`:1是右值,模版中`T &&t`这种为万能引用,右值1传到了`Test函数`中变成了右值引用,但是调用`PrintV`的时候,t变成了左值,因为它变成了一个拥有名字的变量,所以打印`lvalue`,而`PrintV(std::forward<T>(t))`时,会进行完美转发,按照原来的类型转发,所以打印`rvalue,PrintV(std::move(t))`豪无疑问会打印`rvalue`。 - `Test(a)`:a是左值,模版中`T &&t`这种为万能引用,左值a传到Test函数中变成了左值引用,所以打印`lvalue,lvalue,rvalue` - `Test(std::forward<T>(a))`:转发为左值还是右值,依赖于T,T是左值那就转发为左值,T是右值那就转发为右值。 - 补充说明: - 在模板类型推导中,通用引用(T&&)的规则是: - 如果传入的是左值,T 推导为左值引用类型,param 的类型也是左值引用(引用折叠规则:& + && -> &)。 - 如果传入的是右值,T 推导为非引用类型,param 的类型是右值引用(T&&)。 ##5.3、参考deepseek, 另一个完美转发的例子。 - 原则:int&只能绑定左值,int&&只能绑定右值; - 因为T&&可以绑定左值或右值(模板或 auto 推导时),而直接写死的int&&只能绑定右值。 故可以使用模版判断传入值是左值还是右值: ``` template <typename T> void wrapper(T&& arg) { // 如果 arg 是右值,std::forward<T>(arg) 返回右值引用 callee(std::forward<T>(arg)); } void callee(int& x) { std::cout << "lvalue\n"; } void callee(int&& x) { std::cout << "rvalue\n"; } int main() { int x = 10; wrapper(x); // 输出 "lvalue"(左值) wrapper(42); // 输出 "rvalue"(右值) } ``` - T&& 能绑定左值是因为模板推导 + 引用折叠,使其成为“万能引用”。那么为什么 T&& 可以绑定左值?模板类型推导规则: - 如果传入的是 左值(a),T 会被推导为 int&,然后 T&& 变成 int& &&,经过 引用折叠(reference collapsing) 后变成 int&(左值引用)。 - 如果传入的是 右值(20 或 std::move(a)),T 会被推导为 int 或 int&&,T&& 仍然是 int&&(右值引用)。 - int&& 不能绑定左值是因为它是严格的右值引用,没有推导机制。int&& 是明确的右值引用,C++ 标准规定它不能直接接受左值(除非使用 std::move 转换)。 # 6、参考 https://www.cnblogs.com/zhongqifeng/p/16214061.html https://www.cnblogs.com/5iedu/p/11318729.html
上一篇:
点云数据格式bin转为pcd的问题
下一篇:
C++ 部分反射功能的实现(通过字符串获取对象)
0
赞
49 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
立即登录
, 发表评论.
没有帐号?
立即注册
0
条评论
More...
文档导航
没有帐号? 立即注册