跳到主要内容C++11 详解右值引用与移动语义:从性能瓶颈到零拷贝优化 | 极客日志C++算法
C++11 详解右值引用与移动语义:从性能瓶颈到零拷贝优化
C++11 引入右值引用和移动语义以解决对象传递中的深拷贝性能问题。文章阐述了左值与右值的区别,列表初始化机制,以及移动构造和移动赋值的工作原理。通过代码示例展示了如何利用 std::move 转移资源所有权,减少临时对象创建开销。同时分析了编译器优化对返回值的影响,对比了有移动构造函数和无移动构造函数场景下的行为差异,帮助开发者理解如何编写高效 C++ 代码。
KernelLab2 浏览 C++11 详解右值引用与移动语义
C++ 学习参考文档
前情提示
列表初始化:{}
C++98 中的 {}
C++98 中一般数组和结构体可以用 0 进行初始化。
C++11 中的 {}
C++11 以后,想统一初始化方式:试图实现一切对象皆可用{}初始化,{}初始化也叫列表初始化;
内置类型和自定义类型都支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造;
{}初始化的过程中,可以省略掉=;
C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次在有些场景下列表初始化会带来不少便利,如容器 push / inset 多参数构造的对象时,{}初始化会很方便。
C++11 中的 std::initializer_list
上面的{}初始化已经很方便了,但是对于对象容器初始化还是不太方便,比如一个 vector 对象,如果我们想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持——
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {1, 2, 3, 4, 5};
C++11 库中提出了一个 std::initializer_list 的类——
auto il = {10, 20, 30};
std::initializer_list 这个类的本质是:底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。
容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的{x1, x2, x3...}进行初始化。STL 中的容器支持任意多个值构成的{x1, x2, x3, ……}进行初始化,就是 std::initializer_list 的构造函数支持的。
右值引用 && 移动语义
C++98 的 C++ 语法中就有引用的语法,我们前面介绍的就是啦,而 C++11 中新增了的右值引用语法特性,C++11 之后,我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名(给对象取别名,不开空间)。
左值和右值
- 左值:可以取地址。左值是一个表示数据的表达式(如变量名或解引用的指针),一般有持久存在,存储在内存中,可以获取它的地址(区别),左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给他赋值。
- 右值:不能取地址(区别)。右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边。
左值引用和右值引用
概念
Type& r1 = x;
Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名;第二个语句就是右值引用,同样的道理,右值引用就是给右值取别名——这个左值和右值就是上面说的能不能取到地址的区别。
- 左值引用不能直接引用右值,但是 const 左值引用可以引用右值;
- 右值引用不能直接引用左值,但是右值引用可以引用 move(左值)。
template<typename T>
typename remove_reference<T>::type&& move(T&& arg);
move(强转)是库里面的一个函数模板,本质内部是进行强制类型转换。
值得注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
语法层面看,左值引用和右值引用都是取别名,不开空间。
从汇编底层的角度看下面代码中 r1 和 rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要放到一起去理解,很容易搞混!
引用延迟生命周期
右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
int main() {
A aa1;
const A& ref1 = A();
A&& ref2 = A();
cout << "main end" << endl;
return 0;
}
左值和右值的参数匹配问题
C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
右值引用变量在用于表达式时属性是左值,这个设计这里会感觉有点怪,等我们介绍右值引用的使用场景时,就能体会这样设计的价值了。
右值引用和移动语义的使用场景
左值引用主要使用场景
左值引用主要使用场景:函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回:如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。C++11 以后这里可以使用右值引用作为返回值来解决吗?显然这是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
传值返回需要拷贝
移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像 string / vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,本质是要'窃取'引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。
下面的 jqj::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
左值拷贝和右值拷贝:拷贝构造和移动构造
右值引用和移动语义解决传值返回问题
两种场景实践
右值对象构造,只有拷贝构造,没有移动构造的场景
VS2019 的 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
需要注意的是在 VS2019 的 release 版本和 VS2022 的 debug 和 release 版本,下面代码优化为非常恐怖——会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
Linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用的方式关闭构造优化——
g++ test.cpp -fno-elide-constructors
右值对象构造,既有拷贝构造,也有移动构造的场景
VS2019 的 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
需要注意的是在 VS2019 的 release 版本和 VS2022 的 debug 和 release 版本,下面代码优化为非常恐怖——会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
Linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用的方式关闭移动优化——
g++ test.cpp -fno-elide-constructors
以上两种情况结合局部对象生命周期和栈帧的角度的理解
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
下图的左边展示了 VS2019 的 debug 版本以及 -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
需要注意的是在 VS2019 的 release 版本和 VS2022 的 debug 和 release 版本,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
下图的左边展示了 VS2019 的 debug 版本以及 -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
需要注意的是在 VS2019 的 release 版本和 VS2022 的 debug 和 release 版本,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
右值引用和移动语义在传参中的提效
查看 STL 文档我们发现 C++11 以后容器的 push 和 insert 系列的接口否增加的右值引用版本;
当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;
当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上;
把我们之前模拟实现的 bit::list 拷贝过来,支持右值引用参数版本的 push_back 和 insert;
其实这里还有一个 emplace 系列的接口(终于来啦),但是这个涉及可变参数模板,我们需要把可变参数模板介绍完以后再介绍 emplace 系列的接口——涉及引用折叠等内容——
完整代码示例与实践演示
list.h
#pragma once
namespace jqj {
template<class T>
struct list_node {
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T()) :_next(nullptr) , _prev(nullptr) , _data(x)
list_node(T&& x) :_next(nullptr) , _prev(nullptr) , _data(move(x))
};
template<class T, class Ref,class Ptr>
struct list_iterator {
using Self = list_iterator<T, Ref, Ptr>;
using Node = list_node<T>;
Node* _node;
list_iterator(Node* node) :_node(node) {}
Ref operator*()
{ return _node->_data; }
Ptr operator->()
{ return &_node->_data; }
Self& operator++()
{ _node = _node->_next; return *this; }
Self operator++(int)
{ Self tmp(*this); _node = _node->_next; return tmp; }
Self& operator--()
{ _node = _node->_prev; return *this; }
Self operator--(int)
{ Self tmp(*this); _node = _node->_prev; return tmp; }
bool operator!=(const Self& s) const
{ return _node != s._node; }
bool operator==(const Self& s) const
{ return _node == s._node; }
};
template<class T>
class list {
using Node = list_node<T>;
public:
using iterator = list_iterator<T, T&, T*>;
using const_iterator = list_iterator<T, const T&, const T*>;
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
const_iterator begin() const { return const_iterator(_head->_next); }
const_iterator end() const { return const_iterator(_head); }
void empty_init()
{ _head = new Node; _head->_next = _head; _head->_prev = _head; }
list() { empty_init(); }
list(initializer_list<T> il) {
empty_init();
for (auto& e : il) { push_back(e); }
}
template <class InputIterator>
list(InputIterator first, InputIterator last) {
empty_init();
while (first != last) { push_back(*first); ++first; }
}
list(size_t n, T val = T()) {
empty_init();
for (size_t i = 0; i < n; ++i) { push_back(val); }
}
list(int n, T val = T()) {
empty_init();
for (size_t i = 0; i < n; ++i) { push_back(val); }
}
~list() { clear(); delete _head; _head = nullptr; _size = 0; }
list(const list<T>& lt) {
empty_init();
for (auto& e : lt) { push_back(e); }
}
list<T>& operator=(const list<T>& lt) {
if (this != <) {
clear();
for (auto& e : lt) { push_back(e); }
}
return *this;
}
void swap(list<T>& lt) {
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
void clear() {
iterator it = begin();
while (it != end()) { it = erase(it); }
}
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x) { insert(end(), x); }
void push_front(const T& x) { insert(begin(), x); }
void pop_back() { erase(--end()); }
void pop_front() { erase(begin()); }
void insert(iterator pos, const T& x) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
iterator erase(iterator pos) {
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
return next;
}
size_t size() const { return _size; }
private:
Node* _head;
size_t _size = 0;
};
}
Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<algorithm>
#include<string.h>
using namespace std;
namespace Alice {
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
string(const char*) :_size(strlen(str)) ,_capacity(_size) {
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s) {
cout << "string(char* str)-构造" << endl;
reserve(s._capacity);
for (auto ch : s) { push_back(ch); }
}
string(string&& s) {
cout << "string(char* str)-构造" << endl;
swap(s);
}
string& operator=(const string& s) {
cout << "string(char* str)-构造" << endl;
if (this != &s) {
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s) { push_back(ch); }
}
return *this;
}
string& operator=(string&& s) {
cout << "string(char* str)-构造" << endl;
swap(s);
return *this;
}
~string() {
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
void reserve(size_t new_capacity) {
if (new_capacity > _capacity) {
char* tmp = new char[new_capacity + 1];
if (_str) {
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = new_capacity;
}
}
void push_back(char ch) {
if (_size >= _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch) {
push_back(ch);
return *this;
}
const char* c_str() const { return _str; }
size_t size() const { return _size; }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0) {
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1) str += '1';
reverse(str.begin(), str.end());
cout << &str << endl;
return str;
}
}
#include"list.h"
int main() {
jqj::list<Alice::string> lt;
cout << "**************************" << endl;
Alice::string s1("111111111111111111");
lt.push_back(s1);
cout << "**************************" << endl;
lt.push_back("2222222222222222222222222222222222");
cout << "**************************" << endl;
lt.push_back("3333333333333333333333333333333");
cout << "**************************" << endl;
lt.push_back(move(s1));
cout << "**************************" << endl;
return 0;
}
总结
右值引用绑定后,右值引用的属性是左值(本身属性是左值)->要 move 一下。为什么要这样设计?为了让它能够修改!
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,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
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online