C++11 右值引用与移动语义详解
C++ 学习阶段的三个参考文档
看库文件(非官方文档):cplusplus
这个文档在 C++98、C++11 时候还行,之后就完全没法用了……
还可以看语法——准官方文档(同步更新):C++准官方参考文档
这个行,包括 C++26 都同步了,我们以后会看这个。
有大佬——官方文档(类似论坛):Standard C++
这个网站上面会有很多大佬。
总结一下就是——
前情提示
1 C++ 学习的三个参考文档
2 {}初始化
3 C++11 中的{}
4 引用
5 fmin
6 左值引用和右值引用
1 ~> C++11 的历史发展
1.1 历史发展
既然谈到 C++11 的历史发展,那不得不再次献出这种经典老图——
C++11 是 C++ 的第二个主要版本,并且是自从 C++98 版本开始的最重要的更新。C++11 引入了大量更改,标准化了既有的一些实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称'C++0x'——因为它曾被期待在 2010 年之前发布(结果一直拖到 2011 年)。C++03(没出什么新特征)与 C++11 期间花了 8 年时间!这是迄今为止最长的版本间隔——C++08 烂尾了(五年计划),因为 C++08 的规划太大、特性太多——C++ 标准委员会想做的东西太多,结果来不及了,这导致委员会此后调整了发布策略:
从那时起,C++ 有规律地每 3 年更新一次——能出多少是多少。
1.2 拓展讨论
语言被公司使用是有一个过程的。
**C++ 标准委员会:制定理论语法。
编译器:支持 C++ 语法——可进行有原则性的支持(常用的肯定会支持,否则编译器不好用就会被淘汰的)**
举个例子,C 语言的 C99 标准更新了变长数组(用变量),VS 就不支持(编译器不支持的体现)。
包括像 C++23,大多数编译器还不完全支持——
上面这张图,大家可以了解一下:编译器对几个版本的 C++ 的支持情况。
语言的新的语法标准被大规模使用的缓冲期:5~10 年。
(1)大多数公司就是使用到 C++11,当然也有 40%~50% 的公司在使用 C++14(小版本)、C++17(中版本,不少公司用)。
(2)C++23(公司使用偏少)是个大版本:特性还不够成熟(上层的库还不够完善),学的不错的程序员不多;C++23 就更少了。
VS 编译器:MSVC(微软);苹果编译器:Clang(支持苹果的 ObjectC,也支持 C / C++)。
看编译器是否支持语法特性。
2 ~> 列表初始化:{}
2.1 C++98 中的{}
C++98 中一般数组和结构体可以用 0 进行初始化。
2.2 C++11 中的{}
C++11 以后,想统一初始化方式:试图实现一切对象皆可用{}初始化,{}初始化也叫列表初始化;
内置类型和自定义类型都支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造;
{}初始化的过程中,可以省略掉=;
C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次在有些场景下列表初始化会带来不少便利,如容器 push / inset 多参数构造的对象时,{}初始化会很方便。
2.3 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 内部有两个指针分别指向数组的开始和结束。
文档:initializer_list,std::initializer_list 支持迭代器遍历。
容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的{x1 , x2 , x3...}进行初始化。STL 中的容器支持任意多个值构成的{x1 , x2 , x3 , ……}进行初始化,就是 std::initializer_list 的构造函数支持的。
3 ~> 右值引用 && 移动语义
C++98 的 C++ 语法中就有引用的语法,我们前面介绍的就是啦,而 C++11 中新增了的右值引用语法特性,C++11 之后,我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,都是给对象取别名(给对象取别名,不开空间)。
3.1 左值和右值
**左值,可以取地址——**左值是一个表示数据的表达式(如变量名或解引用的指针),一般有持久存在,存储在内存中,可以获取它的地址(区别),左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给他赋值。
**右值,不能取地址(区别)——**右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边。
这里是会编译报错的,根据提示,注释掉后面的 cout 部分就能通过了——
3.2 左值引用和右值引用
3.2.1 概念
Type& r1 = x;
Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名;第二个语句就是右值引用,同样的道理,右值引用就是给右值取别名——这个左值和右值就是上面说的能不能取到地址的区别。
左值引用不能直接引用右值,但是 const 左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用 move(左值)。
template typename remove_reference::type&& move (T&& arg);
move(强转)是库里面的一个函数模板,本质内部是进行强制类型转换,当然这里还涉及一些引用折叠的知识,这个我们后面会详细介绍的,现在先了解一下。
值得注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
语法层面看,左值引用和右值引用都是取别名,不开空间。
从汇编底层的角度看下面代码中 r1 和 rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要放到一起去理解,很容易搞混!
3.2.2 左值引用、右值引用可以相互交叉
左值引用、右值引用可以'相互交叉',如下图所示——
3.2.3 引用延迟生命周期
右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。如下图,演示了右值引用延长匿名对象的生命周期——
3.2.4 左值和右值的参数匹配问题
C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
右值引用变量在用于表达式时属性是左值(天才的设计!),这个设计这里会感觉有点怪,等我们介绍右值引用的使用场景时,就能体会这样设计的价值了,这里先买个关子。
3.3 右值引用和移动语义的使用场景
3.3.1 左值引用主要使用场景
**左值引用主要使用场景:**函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。**左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回:**如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。C++11 以后这里可以使用右值引用作为返回值来解决吗?显然这是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
3.3.2 传值返回需要拷贝
3.3.3 移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像 string / vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,本质是要'窃取'引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的 jqj:string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
3.3.4 左值拷贝和右值拷贝:拷贝构造和移动构造
3.4 右值引用和移动语义解决传值返回问题
3.4.1 两种场景实践
3.4.2 传值返回已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义?
3.4.3 右值对象构造,只有拷贝构造,没有移动构造的场景
如下图展示了 vs2019 的 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
需要注意的是在 vs2019 的 release 版本和 vs2022 的 debug 和 release 版本,下面代码优化为非常恐怖——会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用的方式关闭构造优化——
g++ test.cpp -fno-elide-constructors
运行结果可以看到下图中左边没有优化的两次拷贝。
3.4.4 右值对象构造,既有拷贝构造,也有移动构造的场景
如下图展示了 vs2019 的 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造;右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
需要注意的是在 vs2019 的 release 版本和 vs2022 的 debug 和 release 版本,下面代码优化为非常恐怖——会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用的方式关闭移动优化——
g++ test.cpp -fno-elide-constructors
运行结果可以看到下图中左边没有优化的两次移动——
3.4.5 以上两种情况结合局部对象生命周期和栈帧的角度的理解
3.4.6 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
下图的左边展示了 vs2019 的 debug 版本以及——
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
需要注意的是在 vs2019 的 release 版本和 vs2022 的 debug 和 release 版本,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
3.4.7 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
下图的左边展示了 vs2019 的 debug 版本以及——
g++ test.cpp -fno-elide-constructors
关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
需要注意的是在 vs2019 的 release 版本和 vs2022 的 debug 和 release 版本,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
3.5 右值引用和移动语义在传参中的提效
查看 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 *()
{ _node->_data; }
Ptr ->()
{ &_node->_data; }
Self& ++()
{ _node = _node->_next; *; }
Self ++()
{ ; _node = _node->_next; tmp; }
Self& --()
{ _node = _node->_prev; *; }
Self --()
{ ; _node = _node->_prev; tmp; }
!=( Self& s)
{ _node != s._node; }
==( Self& s)
{ _node == s._node; }
};
< >
{
Node = list_node<T>;
:
iterator = list_iterator<T, T&, T*>;
const_iterator = list_iterator<T, T&, T*>;
{ (_head->_next); }
{ (_head); }
{ (_head->_next); }
{ (_head); }
{ _head = Node; _head->_next = _head; _head->_prev = _head; }
() { (); }
(initializer_list<T> il) { (); (& e : il) { (e); } }
< >
(InputIterator first, InputIterator last)
{ (); (first != last) { (*first); ++first; } }
( n, T val = ())
{ (); ( i = ; i < n; ++i) { (val); } }
( n, T val = ())
{ (); ( i = ; i < n; ++i) { (val); } }
~() { (); _head; _head = ; _size = ; }
( list<T>& lt) { (); (& e : lt) { (e); } }
list<T>& =( list<T>& lt) {
( != <) {
();
(& e : lt) { (e); }
}
*;
}
{ std::(_head, lt._head); std::(_size, lt._size); }
{
iterator it = ();
(it != ()) {
it = (it);
}
}
{ ((), x); }
{ ((), x); }
{ ((), x); }
{ (--()); }
{ (()); }
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = (x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
cur;
--_size;
next;
}
{
_size;
}
:
Node* _head;
_size = ;
};
}
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 {
std::(_str, s._str);
std::(_size, s._size);
std::(_capacity, s._capacity);
}
( string& s) {
cout << << endl;
(s._capacity);
( ch : s) { (ch); }
}
(string&& s) {
cout << << endl;
(s);
}
string& =( string& s) {
cout << << endl;
( != &s) {
_str[] = ;
_size = ;
(s._capacity);
( ch : s) { (ch); }
}
*;
}
string& =(string&& s) {
cout << << endl;
(s);
*;
}
~() {
[] _str;
_str = ;
}
& []( pos) {
(pos < _size);
_str[pos];
}
{
(new_capacity > _capacity) {
* tmp = [new_capacity + ];
(_str) {
(tmp, _str);
[] _str;
}
_str = tmp;
_capacity = new_capacity;
}
}
{
(_size >= _capacity) {
newcapacity = _capacity == ? : _capacity * ;
(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = ;
}
string& +=( ch) {
(ch);
*;
}
{ _str; }
{ _size; }
:
* _str = ;
_size = ;
_capacity = ;
};
{
string str;
end1 = num() - , end2 = num() - ;
next = ;
(end1 >= || end2 >= ) {
val1 = end1 >= ? num1[end1--] - : ;
val2 = end2 >= ? num2[end2--] - : ;
ret = val1 + val2 + next;
next = ret / ;
ret = ret % ;
str += ( + ret);
}
(next == ) str += ;
(str.(), str.());
cout << &str << endl;
str;
}
}
{
jqj::list<Alice::string> lt;
cout << << endl;
;
lt.(s1);
cout << << endl;
lt.();
cout << << endl;
lt.();
cout << << endl;
lt.((s1));
cout << << endl;
;
}
最终运行结果
结尾
本文的内容到这里就全部结束了,感谢您的阅读!