跳到主要内容C++ 右值引用与移动语义详解:利用万能引用实现完美转发 | 极客日志C++算法
C++ 右值引用与移动语义详解:利用万能引用实现完美转发
综述由AI生成右值引用与移动语义是 C++11 核心特性,旨在解决临时对象资源管理的效率问题。通过窃取而非复制资源,移动语义显著降低了内存开销。万能引用结合 std::forward 实现了完美转发,使模板函数能保留参数的左值或右值属性,避免不必要的类型转换与拷贝,是现代高性能 C++ 开发的关键技术。
橘子海12 浏览 一、左值和左值引用
左值是表示数据的表达式,如变量名或解引用的指针。我们可以获取它的地址,也可以对它进行赋值操作。左值可以出现在赋值符号的左边,而右值不能。定义时若用 const 修饰后的左值,虽然不能给它赋值,但可以取它的地址。
左值引用就是给左值的引用,相当于给左值取别名。
int main() {
int* p = new int(0);
int b = 1;
const int c = 2;
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
二、右值和右值引用
右值也是表示数据的表达式,例如字面常量、表达式返回值、函数返回值(非左值引用返回)等。右值可以出现在赋值符号的右边,但不能出现在左边,且通常不能取地址。
右值引用就是对右值的引用,给右值取别名。
#include <string>
std::string to_string(int val) {
std::string str;
return str;
}
int main() {
double x = 1.1, y = 2.2;
10;
x + y;
int&& rr1 = 10;
double&& rr2 = x + y;
std::string&& rref3 = std::();
std::string&& rref4 = ();
;
}
string
"1111"
to_string
123
return
0
右值分类
- 纯右值: 内置类型 (不需要多关注)
- 将亡值: 自定义类型 (匿名对象,临时对象)
int main() {
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5;
return 0;
}
虽然右值本身不能取地址,但给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。例如不能直接取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 右值引用对象取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用。这个特性在实际右值引用的使用场景中并不重要,了解即可。
三、如何判断左值和右值
不能只通过是否能被修改去判断左、右值,它们之间的区别在于左值和右值是否能取地址。右值是临时性的,其内容通常不可修改,只能通过移动语义 (move) 将其资源转移到其他对象。虽然右值引用变量本身可以绑定到右值,但是它本身并不能直接修改右值的内容。
左值: 可以取地址,也可能是一个表达式
右值: 不可以取地址的
四、左值引用与右值引用比较
左值引用总结: 通常左值引用只能引用左值,不能引用右值;const 左值引用即可引用左值,也可以引用右值 (临时性)。
int main() {
int a = 10;
int& ra1 = a;
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结: 右值引用只能引用右值,不能引用左值;但是右值引用可以 move 以后的左值。
int main() {
int&& r1 = 10;
int a = 10;
int&& r2 = a;
int&& r3 = std::move(a);
return 0;
}
五、右值引用与移动语义(重点)
5.1 彻底解决传值返回拷贝问题
void func1(const std::string& s);
std::string& func2();
左值引用做返回值的问题没有彻底解决。如果返回值是 func2 中局部对象,不能用引用返回,只能通过传值返回。那么传值返回会导致至少 1 次拷贝构造 (如果是一些老一点的编译器可能是两次拷贝构造,没有进行优化)。
问题:既然左值引用做返回值不行,那么这里是将右值引用,使用右值引用做返回值是否能解决该问题?
首先无论是左值引用还是右值引用,面对局部变量出了作用域就会销毁,无法进行干预。对此右值引用不能解决局部变量的问题,是用于解决局部变量所掌握资源的生命周期的问题。
5.2 右值引用和移动语义延长局部变量掌握资源生命周期
在自定义 string 类中增加移动构造。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
string(string&& s) : _str(nullptr), _size(0), _capacity(0) {
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
这段代码的逻辑是:参数部分专门用来接收右值 (将亡值)。但是右值引用属性是左值 (这一点后面会谈论),将亡值意味着生命周期十分短暂,这样意味着将亡值所掌握的资源也会销毁,这样子就会导致十分浪费。对此直接 swap(s) 夺舍他的资源,反正你的生命不多了,不如将资源交给我,所以它叫做移动构造。
这样可以解决面对传值返回导致深拷贝,减低效率的问题。这里不需要深拷贝,只需要交换下资源就行了。
这里移动构造没有解决或延长临时变量生命周期,而是延长了资源的生命周期。
5.3 移动构造隐含代价
int main() {
std::string s1("1111111111111");
std::string s2 = s1;
std::string s3 = std::move(s1);
return 0;
}
由于移动构造就是窃取别人的资源来构造自己。那么当发生移动构造,被窃取的那一方就会被盗取 (资源属于别人的)。所以在使用移动构造时,需要保证被窃取对象是不健康 (不需要的),所以 s1 不要随便变成右值属性。自己的器官给了就是自己没有了。
5.4 移动赋值
在自定义 string 类中增加移动赋值函数,再去调用 bit::to_string(1234),不过这次是将 bit::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
string& operator=(string&& s) {
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main() {
bit::string ret1;
ret1 = bit::to_string(1234);
return 0;
}
string(string&& s)
string& operator=(string&& s)
里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string 函数中会先用 str 生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把 str 识别成了右值,调用了移动构造。然后在把这个临时对象做为 bit::to_string 函数调用的返回值赋值给 ret1,这里调用的移动赋值。
5.5 编译器的优化
分析图一: 这里 str 属性为左值,在传值返回时进行拷贝构造生成临时变量,临时变量具有常量属于为右值,通过右值进行初始化走移动构造。
分析图二: 这里 str 属性为左值,如果不 move 话,需要走一次拷贝构造那么销毁较大,编译器优化将 str 返回时,将 str 属性转化为右值属性,进行移动构造,由于接下需要再移动构造,编译器优化后,合二为一只需要一次移动构造即可,这里把 str 处理成右值,相当于 move 一下。
5.5.1 同性质函数进行优化
这里无论是拷贝构造 + 赋值拷贝,还是移动构造 + 移动赋值,编译器都无法优化。因为这里两个不是同一个性质的函数,编译器不好合二为一,没有强行参与。这里重点还是分析图二右边的图,帮助我们更好地深入了解移动语义。
具体分析过程: 首先这里有临时对象进行移动构造,首先临时对象先跟 str 交换下,临时对象指向 str 原本指向的资源,str 指向临时对象原本指向的空间 (空),之后 str 生命周期结束,在被 ret1 接收时,又进行了一次移动语义,ret1 一开始指向了一块资源空间,临时对象是将亡值,把这块指向空间带走,帮我释放空间,重演了一遍刚刚的操作,真是一举两得。这里原本要释放三个资源,两次拷贝演变成只需要释放一个资源没有拷贝资源,效率得到了大幅度的提升。
六、现代写法和移动语义的区别
C++11 中,swap 的现代写法和移动语义有一些关联,都是进行交换,而现代写法是窃取已经完成拷贝的资源,效率上没有提高,形式上简洁了许多,而移动语义是直接窃取资源,大幅度提高了效率。
七、右值引用本身属性
从图可以得到,右值引用本身就是左值。只有右值引用本身处理成左值,这样的才能实现移动构造和移动赋值,转移资源语法是自洽的。
右值引用的属性如果是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的,那么 swap 需要修改是行不通的 (可以理解为右值带有 const 属性)。
八、move 函数
当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值。C++11 中,std::move() 函数位于 <utility> 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) noexcept {
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
九、STL 容器插入接口函数也增加了右值引用版本
void push_back(value_type&& val);
int main() {
list<bit::string> lt;
bit::string s1("1111");
lt.push_back(s1);
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
string(const string& s)
string(string&& s)
string(string&& s)
push_back 就是涉及到拷贝的目标,这里匿名对象就行。可以看出移动构造几乎没有代价,移动资源就行。
十、右值引用导致属性改变
这里调用 insert 进行复用,但是这里 x 是右值引用接收,由于属性变为左值属性,这里需要对 insert 进行函数重载,每一层都要有一个右值引用的版本。如果使用万能引用就可以大程度减少这份部分工作。
如果每一层都需要考虑属性发生变化,当复用接口的过程中,想要保持右值属性需要去 move 一下,会导致十分麻烦和花时间。
处理保证是右值 (用于属性的改变和 move 的转化),上面效率提升。针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数,对于内置类型,和浅拷贝自定义类型,没有移动系列函数。如果是内置类型,没有拷贝构造和移动构造的说法,但是有属性的问题。
十一、万能引用及其完美转发
11.1 模板中&&万能引用
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) {
Fun(t);
}
int main() {
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(std::move(a));
const int b = 8;
PerfectForward(b);
PerfectForward(std::move(b));
return 0;
}
模板中 && 不代表右值引用,而是万能引用。万能引用确实使得函数模板能够接受任何类型的参数(左值和右值),根据参数属性自动推导左值引用还是右值引用的能力。
引用类型的唯一作用就是限制了接收的类型,万能引用的特性使得函数模板能够更加灵活地处理传递给它的参数。
虽然不需要考虑在每一层写函数重载了,但是从打印结果来看,没有解决后续使用中右值都退化成了左值。如果我们希望能够在传递中保持它的左值或者右值的属性来解决上述问题。
11.2 std::forward 完美转发
完美转发是指在函数模板中以保持类型不变的方式传递参数,包括参数的值类型 (左值还是右值)。
std::forward 完美转发在传参的过程中保留对象原生类型属性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) {
Fun(std::forward<T>(t));
}
int main() {
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(std::move(a));
const int b = 8;
PerfectForward(b);
PerfectForward(std::move(b));
return 0;
}
11.2.1 是否可以通过强制类型转化达到保持值属性
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) {
Fun((T&&) t);
}
int main() {
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(std::move(a));
const int b = 8;
PerfectForward(b);
PerfectForward(std::move(b));
return 0;
}
虽然从结果上是一样的,但是强制类型转换 (T&&) t 确实只是将 t 强制转换为 T&& 类型,但它并没有保证将 t 的原始值类别(左值或右值)正确地传递给被调用的函数。这种转换方式可能会导致不正确的参数传递行为,推荐使用 std::forward(t) 在传参的过程中保持了 t 的原生类型属性。
11.3 关于完美转发的使用场景
#include <iostream>
template<class T>
struct ListNode {
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List {
typedef ListNode<T> Node;
public:
List() {
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x) {
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x) {
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x) {
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x) {
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
在代码中使用万能引用(也称为转发引用,T&&)和 std::forward 可以提高代码的效率,尤其是当你在函数内部需要处理既可以是左值,也可以是右值的场景时。通过使用万能引用结合完美转发 (std::forward),你可以在保留对象原本的值类别(左值或右值)的同时,将其传递给其它函数,避免不必要的拷贝操作,从而提升性能。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online