接上文,我们先是讲到了c++11的新增语法:列表初始化,c++11让所有的初始化方法进行了大一统。C++11新增语法:列表初始化-CSDN博客
本文我们来讲c++11里最为重要的语法之一:右值引用与移动语义。让我们来看看,到底是怎样的语法,让c++代码的运行效率更上台阶,跑得更快
1.左值和右值
左值:
左值是一个表示数据的表达式。具有持久态,一般存在于内存中。我们可以取到它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。但是由const修饰的左值是无法出现在赋值符号的左边的。
右值:
右值是一个表示数据的表达式。不具有持久态,要么是字面量常量,要么是表达式产生的临时对象。右值只能出现在赋值符号的右边,且不能取地址。
左右值的核心区别就是是否可以取地址:左值可以取地址,右值不能取地址。
#include<iostream>
using namespace std;
int main()
{
//左值可以地址
//p,b,c,*p,s,s[0]是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
//右值不能取地址
double x = 1.1, y = 2.2;
//10、x + y、fmin(x, y)、string("11111")都是常见的右值
10;
x + y;
fmin(x, y);
string("11111");
}
在上面的例子中,左值相信大家没什么问题,毕竟都是变量。下面我们来看看右值:1. 10这是一个字面量常量不能取地址。 2.x+y,这是一个求值表达式,返回的是临时对象,也不能取地址。 3. fmin(x,y) 这是一个传值返回的函数,返回的仍然是一个临时对象,不能取地址。 4. string("111111") 这是一个匿名对象,一个变量没有变量名当然不能取地址了
2.左值引用和右值引用
Type& l = x; Type&& r = y; 第一个表达式就是左值引用,而第二个表达式就是右值引用。左值引用:引用左值。右值引用:引用右值
左值引用不可直接引用右值,但const + 左值引用可以引用右值
右值引用不可直接引用左值,但右值引用可以引用move(左值)
move是c++库里面的一个函数模板,其本质是强制类型转化。这个函数里面涉及就一些引用折叠的情况,这个我们后面再将。这里我们先找到它的作用就是将左值转化为右值
这里需要注意,变量变量表达式都是左值的。也就意味着当右值被右值引用引用后,其右值引用变量的属性是左值的
在语法层面上,左值引用与右值引用都是取别名,不开空间的。在底层来看,其本质都是指针,没有上面区别。但是在使用的时候我们就只看语法层面,不然会陷入逻辑漩涡。
#include<iostream>
using namespace std;
int main()
{
//左值可以地址
//p,b,c,*p,s,s[0]是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
//左值引用 引用左值
int*& lp = p;
int& lb = b;
const int& lc = c;
int& lP = *p;
string& ls = s;
char& ls0 = s[0];
//右值不能取地址
double x = 1.1, y = 2.2;
//10、x + y、fmin(x, y)、string("11111")都是常见的右值
10;
x + y;
fmin(x, y);
string("11111");
//右值引用 引用右值
int&& r = 10;
double&& rtmp = x + y;
double&& rfmin = fmin(x, y);
string&& rs = string("11111");
//左值引用不能直接引用右值,要加上const。既const左值可以引用右值
const int& r1 = 10;
const double& rtmp1 = x + y;
const double& rfmin1 = fmin(x, y);
const string& rs1 = string("11111");
//右值引用不能直接引用左值,但当左值被move了之后,就可以引用了
int*&& lp1 = move(p);
int&& lb1 = move(b);
const int&& lc1 = move(c);
int&& lP1 = move(*p);
string&& ls1 = move(s);
char&& ls2 = move(s[0]);
//右值引用本身是变量,所以它本身属性也是左值属性。而右值才是右值属性不能取地址
cout << &lp1 << endl;
cout << &lb1 << endl;
cout << &lc1 << endl;
cout << &lP1 << endl;
cout << &ls1 << endl;
cout << (void*)&ls2 << endl; //cout会将char*视为C风格字符串(从该地址开始输出字符,知道遇见'\0')
//而其他类型的指针会看作普通地址输出,所以为了正常输出其地址,将其强转为void*
}
3.引用延长生命周期
右值引用可以延长临时对象、匿名对象的生命周期。const左值引用也可以延长其生命周期,但是不能修改其值。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1 = "Yuzuriha";
string&& str = s1 + s1; //引用s1+s1表达式产生的临时对象
cout << "str: " << str << endl;
str = "abc";
cout << "str: " << str << endl<<endl;
const string& str1 = s1 + s1;
cout << "str1: " << str1 << endl;
//str1 = "abc";
//cout << "str1: " << str1 << endl;
}
右值引用可以修改其内容,但const左值引用不可修改,会直接报错
4.左值和右值的参数匹配
C++98中,我们知道一个函数的参数为const左值引用,实际可以匹配左值应用也可以匹配右值引用
C++11中,分别重载参数为左值引用、const左值引用、右值引用的函数 f ()。左值引用则会匹配f (左值引用),const左值引用匹配f (const左值引用),右值引用匹配f (右值引用)。
#include<iostream>
using namespace std;
void f(int& n)
{
cout << "左值引用匹配" << endl;
}
void f(const int& n)
{
cout << "const左值引用匹配" << endl;
}
void f(int&& n)
{
cout << "右值引用匹配" << endl;
}
int main()
{
int a = 1;
const int b = 1;
f(a); //左值引用匹配
f(b); //const左值引用匹配
f(1); //右值引用匹配
//注意
int&& x = 1;
f(x); //右值引用仍是变量,既左值。所以匹配左值
f(move(x)); //move之后变成了右值。所以匹配右值
}
5.右值引用和移动语义的使用场景
5.1左值引用使用场景
左值引用的主要应用场景是函数的传参和传返回值,左值引用已经解决了大部分这种情况的拷贝,极大了提高了代码运行效率。
但是当返回值是函数内部的局部变量时,我们就无法使用左值引用返回返回值。因为函数的局部变量出了作用域就销毁了,这时我们如果使用左值引用返回,就相当于使用了野指针一样,是不可取的。
这种情况在C++98中,如果要选择返回其返回值,就只能传值返回。而在C++11中我们则可以使用右值引用返回。但是这里我所说的右值引用并不是直接的将返回值类型改成右值引用就可以解决的。因为返回值始终是局部变量,出了作用域始终要销毁,右值引用并不能改变返回值的存储位置。所以修改返回值类型是没有用的,我们要用的是移动语义来实现(移动构造、移动赋值)
class Solution
{
public:
// 传值返回需要拷贝
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());
return str;
}
};
class Solution
{
public:
// 这里的传值返回拷贝代价就太大了
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
};
5.2移动构造和移动赋值
移动构造是一个函数,类似于拷贝构造,和拷贝构造构成函数重载。其第一个参数是自身类型的引用,但不同的是这个引用必须是右值引用。如果还有其他参数,那么其他参数都必须有缺省值。
移动赋值也是一个函数,类似于赋值重载,和赋值重载构成函数重载。其第一个参数是自身类型的引用,但不同的是这个引用必须是右值引用。
移动构造和移动赋值必须在有深拷贝的类中才有意义,例如vector、string中。移动构造和移动赋值的本质“窃取”右值对象的内容,并不是像拷贝构造和赋值重载函数那样取拷贝资源。所以移动构造和移动赋值可以极大的提升代码运行效率。
下面是hyc::string实现的移动构造和移动赋值样例
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
namespace hyc
{
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* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "拷贝赋值" <<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 << "移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
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;
int _size = 0;
int _capacity = 0;
};
}
int main()
{
//构造
hyc::string s("1111");
//拷贝构造
hyc::string s1 = s;
//移动构造
hyc::string s2(move(s));
//移动赋值
hyc::string s3;
s3=move(s);
cout << "XXXXXXXXXXXXXXXXXXXXX" << endl;
}
5.3右值引用和移动语义解决传值返回问题
namespace hyc
{
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 << "******************************" << endl;
return str;
}
}
//场景1
int main()
{
hyc::string ret = hyc::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
hyc::string ret;
ret = hyc::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
5.3.1 右值对象构造,只有拷贝构造,没有移动构造
下图左边展示了 VS2019 Debug 和 g++ test.cpp -fno-elide-constructors关闭优化环境下编译器的处理:一次拷贝构造,一次拷贝赋值。
需要注意的是,在 VS2019 Release 和 VS2022 Debug/Release下,以下代码会进一步优化:直接构造要返回的临时对象,str 本质是临时对象的引用(底层角度用指针实现)。从运行结果来看,str 的析构发生在赋值之后,这说明 str 是临时对象的别名。
5.3.2 右值对象构造,有拷贝构造,也有移动构造的场景
下图展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
需要注意的是在 vs2019 的 release和 vs2022 的 debug 和 release下,下面代码优化为非常恐怖,会直接将 str对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,下图所示。
linux下可以将下面代码拷贝到 test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次移动。
5.3.3 右值对象赋值时,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
下图展示了VS2019 Debug和 g++ test.cpp -fno-elide-constructors关闭优化环境下编译器的处理:一次拷贝构造,一次拷贝赋值。
需要注意的是在 VS2019 的 Release 和 VS2022 的 Debug/Release 下,下面代码会进一步优化:直接构造要返回的临时对象,str本质是临时对象的引用(底层角度用指针实现)。从运行结果来看,str 的析构发生在赋值之后,说明 str 就是临时对象的别名。
5.3. 4右值对象赋值时,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值
下图展示了 VS2019 Debug 和 g++ test.cpp -fno-elide-constructors关闭优化环境下编译器的处理:一次移动构造,一次移动赋值。
需要注意的是,在 VS2019 Release 和 VS2022 Debug/Release下,以下代码会进一步优化:直接构造要返回的临时对象,str 本质是临时对象的引用(底层角度用指针实现)。从运行结果来看,str的析构发生在赋值之后,这说明 str 是临时对象的别名。
5.4右值引用和移动语义在传参中的提效
查看C++11及其以后的版本,所有容器的insert、push系列的接口,都增加了右值引用版本
当实参是左值时,会调用拷贝构造、赋值重载,将内容拷贝到容器
当实参是右值是,会调用移动构造、移动赋值,将内容直接交换到容器中
int main()
{
list<hyc::string> lt;
hyc::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(hyc::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
5.5总结
在传值返回的这种情况下,我们无法使用传左值引用返回,只能使用传值返回。而在C++11之前,只能通过编译器的优化提高代码的运行效率。
但是在C++11之后,使用移动语义可以彻底解决这个效率问题。即使代码一点不优化,面对移动构造、移动赋值极其高的效率而言,不痛不痒。C++11之后编译器的优化不再是雪中送碳,而是锦上添花。
只有需要深拷贝的类才有实现移动构造、移动赋值的价值。毕竟浅拷贝的代价本就很小。
6.引用折叠
C++中不能直接定义引用的引用,例如:int&& &r = i。但是可以通过typedef或者模板实现引用的引用
当通过typedef或者模板实现的引用的引用,就会发生“引用折叠”。既右值引用去引用右值引用为,会“引用折叠”为右值引用。左值引用去引用任何引用时,会“引用折叠”为左值引用。
#include<iostream>
using namespace std;
// 由于引用折叠限定,f1实例化以后总是⼀个左值引用
template<class T>
void f1(T& x)
{
}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T && x)
{
}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
//f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
//f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
//f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
//f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
//f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
//f2<int&&>(n); // 报错
f2<int&&>(0);
}
在上述代码中,我们将下面的代码称为“万能引用”。因为传左值引用即为左值引用,传右值引用即为右值引用
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T && x)
{
}
在实际中,我们并不会显示的写出模板类型,而是让模板制动推导类型
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
}
7.完美转发
上述代码中我们知道,万能引用的模板函数 Function(T&& t),传左值引用就实例化左值引用,传右值引用就实例化右值引用。
但是结合我们上面讲的,右值引用仍是属于变量,既右值引用的属性仍为左值。如果当万能引用的模板函数里,还需要继续使用 t 去匹配左值引用\右值引用时。这个时候就会出现错误:不论如何匹配都是左值引用。
此时如果我们还想继续维持它本身的属性,就需要使用到完美转发
完美转发:forward,是一个模板函数。他主要通过引用折叠的方式实现: 当传递给 Function 的实参是右值时,模板参数 T 被推导为 int(无折叠),forward 内部将 t 强转为右值引用返回; 当传递给 Function的实参是左值时,模板参数 T被推导为 int&,经引用折叠后变为左值引用,forward 内部将 t 强转为左值引用返回。
#include<iostream>
using namespace std;
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<class T>
void Function(T&& t)
{
Fun(t);
}
int main()
{
//10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
//a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
//b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
Function(b); // const 左值
//std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
}
不使用完美转发时,全部都为左值引用
#include<iostream>
using namespace std;
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<class T>
void Function(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
//10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
//a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
//b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
Function(b); // const 左值
//std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
}
使用完美转发,运行出正确结果