目录
左值引用和右值引用
右值引用使用场景和意义
移动语义
传值返回问题
移动构造
移动赋值
总结
解决传值插入问题
完美转发
模板中的&&万能引用
完美转发std::forward
完美转发实际中的使用场景
左值引用和右值引用
其实在C++11之前,C++没有左右值之分,只有一种引用。而在传统的C++语法中的引用的语法存在的情况下,C++11中又新增了的引用语法特性,就有了左值引用和右值引用之分,所以从现在开始我们之前学习的引用就叫做左值引用。
哪什么是左值引用和右值引用,在左边的一定是左值引用,在右边的一定是右值引用吗?
答:不是的!
什么是左值?什么是左值引用?
左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。无论左值引用还是右值引用,都是给对象取别名。左值也有一个例外(const),我们可以获取它的地址,但是不能将其放在赋值符号的左边,不能被赋值修改,所以我们需要分别的看待。
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,但是左值引用的核心:我们可以获取它的地址,因为基本上百分之99的值,左值引用都可以对它进行赋值。但是定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值起别名。
什么是左值?
我们可以获取它的地址的就是左值。
int main()
{
//左值(表示数据的表达式):可以取地址 + (可以对它赋值<--不是一定的)
int a = 10;
const int b = 20; //b左值,但是不可以对它赋值
//b = 10; /* error */
int* p = &a;
*p = 100; //左可以是表达式所以*p是
return 0;
}
什么是左值引用?
对左值取别名的就是左值引用,即:以前所学的引用都叫做左值引用。
int main()
{
// 以下的p、b、c、*p都是左值
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;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(主要指的是传值返回)(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,右值引用就是给右值取别名。
什么是右值?
右值的特点:不能取地址。取地址会报不是左值的错误。
double fmin(double x, double y)
{
double min = x;
if (min > y)
return y;
return x;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值:不能取地址
10;
x + y;
fmin(x, y);
return 0;
}
为什么 x+y 与 fmin(x, y) 会产生一个右值呢?因为 fmin(x, y) 是传值返回,会产生一个临时对象,x+y 也会产生一个临时对象,所以可以认为临时对象就是右值。
什么是右值引用?
右值引用与左值引用的区别是:两个 ‘&’ 。右值引用是给右值取别名。
double fmin(double x, double y)
{
double min = x;
if (min > y)
return y;
return x;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左值引用能引用右值吗?
左值引用是不能直接的引用右值的:
int main()
{
double x = 1.1, y = 2.2;
double& sum = x + y; // error C2440: 无法从“double”转换为“double & ”
return 0;
}
右值最大的缺陷就是不能被改变的,左值引用的意义是能被改变的,于是左值引用需要加一个 const,即:const的左值引用才能引用右值。
int main()
{
double x = 1.1, y = 2.2;
const double& sum = x + y;
return 0;
}
利用左值引用引用右值的意义:正如我们前面的引用所学。当函数传参接受的时候:
template<class T>
int FunC(T x){}
x 与 y 都可以通过拷贝,接收左值或右值的数据,但是拷贝毕竟是花费额外的空间,如果T为自定义类型的时候,过大,无非是浪费空间,于是变有了以下操作:
template<class T>
int FunC(T& x){}
然而,在进行此操作的时候有一条建议:当x值不需要被修改的时候,甚至不能被修改的时候,建议设置为const修饰:
template<class T>
int FunC(const T& x){}
这是对不能被修改的数据的保护,也是让其在能接收左值的同时也能接收右值。当然了,当需要被修改的时候也就不能用const修饰了,也就不能传右值了。
右值引用能引用左值吗?
右值引用是不能直接的引用左值的。
int main()
{
int a = 10;
int&& r2 = a; // error C2440: “初始化”: 无法从“int”转换为“int &&”
return 0;
}
#include<utility>
using namespace std;
int main()
{
int a = 10;
// 右值引用可以引用move以后的左值
int&& r3 = move(a);
return 0;
}
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,编译器会为其开辟一个空间,然后将10存进去,再引用。也就是说,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,实际中右值引用的使用场景并不在于此,这个特性也不重要。(作为了解)
#include<iostream>
using namespace std;
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
cout << &rr1 << endl;
cout << &rr2 << endl;
return 0;
}
右值被右值引用之后会变成左值。
右值引用使用场景和意义
问:先不说左值引用与右值引用,就说引用是用来干什么的?
引用的价值:减少拷贝(拷贝的代价很大,尤其是深拷贝)
本来就在C++11之前的应用就可以引用左值也可以引用右值,就是加const罢了,没有任何的问题。所以引用的核心价值就是在减少拷贝。自C++11之后区分的就更加详细了些:
- 左值引用解决的问题:
- 做参数: a. 减少拷贝,提高效率 b. 做输出性参数,将修改值带回,如:swap函数
- 做返回值: a. 减少拷贝,提高效率 b. 引用返回,可以修改返回对象,如:operator[]
看起来左值引用解决的很完美,其实左值引用只是解决了绝大多数的问题。做返回值的问题其实解决的不行。 只解决了百分之70的问题。
比如;将数值转为string的函数,左值引用是解决不利了的,左值引用不敢用。
to_string是经典的传值返回,因为如果我们简单的实现to_string,会发现:
#include<string>
using namespace std;
namespace cr
{
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str; // str在这个地方是一个局部对象 -- 出作用域就销毁了,根本不敢用左值引用
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
}
str在to_string函数中是一个局部对象,是出了作用域就会销毁,所以根本不敢用左值引用。如果用左值引用这里就崩了,因为返回了它的别名,但是其其实已经析构了。
string& to_string(int value) // 左值引用程序会崩溃
{
//……
string str; // str在这个地方是一个局部对象 -- 出作用域就销毁了,根本不敢用左值引用
//……
return str;
}
对于这种类似的情况,有一道题也是同样的:
118. 杨辉三角https://leetcode.cn/problems/pascals-triangle/description/
如果是vector<int>也就算了,还能接受,但是其是vector<vector<int>>这样的类型,其数据量将会是恐怖的,但是又因为是内部创建的局部变量,出了作用域就调用析构函数,所以根本不敢使用左值引用。
C++98的左值引用处理上面的场景是很难的。
用C++98处理只会有:
- 使用全局处理:
是不好的,会有安全问题,线程安全问题。在多线程同时调用这个函数的时候,是会为了此资源发生竞争的问题。(会有多线程的程序,谨慎使用静态变量与全局变量)
- 使用new:
会有内存泄漏的问题,new出来的空间,是需要使用delete的,万一忘记了。并且,有时候会在想到释放的时候抛异常,就会有内存泄漏的问题。
- 使用输出型参数:
这是C++98最好的处理方式。也是公司所会采取的方式。
但是此方法也是有不好的地方,其是不太符合使用习惯的。
移动语义
传值返回问题
C++11右值引用一个重要的功能就是解决上面的情况:
右值引用的使用方法是与左值应用的使用方法截然不同的。下面研究右值引用是怎么使用的,并且是如何做到的:
(为方便讲解,此处使用一个模拟实现的string容器,未添加移动构造与移动赋值)
#include<string>
#include<iostream>
#include<assert.h>
using namespace std;
namespace cr
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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 << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
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];
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;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
}
int main()
{
cr::string ret = cr::to_string(1234);
return 0;
}
cr::string ret = cr::to_string(1234); 调用了几次拷贝构造?
调用了2次拷贝构造,编译器优化成了一次拷贝构造。
Note:
也有的编译器是一次拷贝构造都没有进行,比如Linux的g++,因为g++一看你虽然搞出来了一个ret的值,但是你并没有去使用它,所以直接优化的一次也不给你拷贝了。这种时候就需要使用ret一下才会有一次拷贝构造。因为Linux的g++的默认情况是Release版本。优化会更凶。
可以利用Linux,通过参数-fno-elide-constructors关闭g++的编译优化,打印未优化时调用的结果:
而为了压入调用to_string函数的返回值,而在main函数开辟的空间,是在该语句结束后销毁,即在拷贝给ret后销毁。
优化成调用一次拷贝构造,其实就是相当于它把中间生成的值给干掉了,就是编译器觉得很多余,既然是给ret的,那直接给以str给ret就行了,直接一步到位。
问:既然这个样子,那为什么编译器要设置一个临时栈帧存这个临时变量?不要岂不是更好?
答:不可以!
因为在有一些场景下,如定义后,其在一些列的操作下再使用+=,就是不能被省略优化的了(这个地方需要移动赋值解决):
int main()
{
cr::string ret;
// …… 一系列的操作
ret = cr::to_string(1234);
return 0;
}
因为,ret定义与to_string并不是在一个地方的,只有在一条语句下才能优化。
此处,最后多一个拷贝构造是因为,string容器的模拟实现的operator=的实现方法:所以,将其看作一次拷贝构造(深拷贝),一次拷贝赋值(深拷贝)即可。因为这的拷贝赋值由一次拷贝构造实现。
问:那这些地方怎么办呢?右值引用又是如何起的作用?
右值引用不是直接起作用而是间接起作用。右值引用在这个地方增加了两个函数。
原本在C++98是拥有,拷贝构造与拷贝赋值。
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
C++98是通过const string& s可以引用右值的特点执行。
C++11又提供增加了,移动构造与移动赋值。
移动构造
移动构造与拷贝构造的区别:
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动资源" << endl;
swap(s);
}
其利用了,编译器会按照,最符合该路径的路径执行,虽然拷贝构造可以执行右值,但是移动构造更加符合右值,于是右值都执行移动构造。
(只添加了移动构造的string模拟容器)
#include<string>
#include<iostream>
#include<assert.h>
using namespace std;
namespace cr
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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 << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动资源" << endl;
swap(s);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
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];
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;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
}
C++11增加了对右值的定义同时,将其划分为:
- 内置类型右值 -- 纯右值
- 自定义类型右值 -- 将亡值
之所以叫将亡值,是因为如字面常量、表达式返回值,函数返回值返回,其在执行完它的那条语句之后,就会销毁,就死亡了。而移动构造就是抓住自定义类型右值死亡销毁之前,与其做一条交易,将数据给我,你都将死亡了,带着这些数据也没有用,也就是带去销毁,给我吧。
int main()
{
cr::string str1("hello");
cr::string str2(str1);
cr::string str3(move(str1)); // move之后的左值变为右值
return 0;
}
之所以其的数据_str = nullptr,size = 0, capacity = 0。是因为移动构造的定义:
移动构造的价值:
在拥有移动构造后的:
int main()
{
cr::string ret = cr::to_string(1234);
return 0;
}
调用的不是一次拷贝钩构造了,而是一次移动构造。
此处,只进行一次移动构造的原理:
未优化的情况:
虽然str是左值,但是编译器发现执行完return str; 就函数结束了,str销毁了,于是将其看作了右值, 于是执行移动构造。
优化后的情况:
优化成调用一次移动构造,其实就是相当于它把中间生成的值给干掉了,就是编译器觉得很多余,既然是给ret的,那直接给以str给ret就行了,直接一步到位。
· 拷贝构造的代价 > 移动构造的代价。因为拷贝构造拷贝了还要将旧的资源进行释放。是一次深拷贝加一次前面的值的释放。而移动构造是将资源转移过来,不需要拷贝。
右值引用此处最重要的功能:透过移动构造,在传值的场景下来减少拷贝。
移动赋值
此处是移动构造与移动赋值都为存在:
一方面:由于移动构造会在未有移动赋值时起作用,而此处本应移动构造起作用的点。所以此处忽略掉移动构造。
二方面:移动构造与移动赋值本就是在C++11同时出现,为的是解决C++98的问题。所以只有在忽略掉移动构造才能凸显移动构造解决的C++98问题。
int main()
{
cr::string ret;
// ……一系列操作
ret = cr::to_string(1234);
return 0;
}
这个点也是在移动构造篇中讲解到的C++98无法优化的场景。
此处,最后多一个拷贝构造是因为,string容器的模拟实现的operator=的实现方法:所以,将其看作一次拷贝构造(深拷贝),一次拷贝赋值(深拷贝)即可。因为这的拷贝赋值由一次拷贝构造实现。
移动赋值与拷贝赋值的区别:
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
而在同时加入移动构造与移动赋值之后:
(同时添加了移动构造与移动赋值的string模拟容器)
#include<string>
#include<iostream>
#include<assert.h>
using namespace std;
namespace cr
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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 << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动资源" << endl;
swap(s);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << 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 n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
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;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
reverse(str.begin(), str.end());
return str;
}
}
int main()
{
cr::string ret;
// ……一系列操作
ret = cr::to_string(1234);
return 0;
}
在将将亡值str会带走得资源换下来并保留继续使用的同时,并将ret原有的无用资源交给将亡值str让其带走。
C++11将STL库也进行了更新,此处举例几个容器:
string容器的构造函数相关文档
string容器的operator=相关文档
vector容器的构造函数相关文档
vector容器的operator=相关文档
总结
移动构造与移动赋值在拷贝构造与拷贝赋值的角度上大大的节省了空间,也大大的提升了效率。
对于函数的传值返回,右值引用不是直接起作用的,与左值引用不同。右值引用是通过移动构造与移动赋值起作用的。通过函数的传值返回是一个右值(将亡值),然后通过转移它的资源来减少拷贝。
拷贝构造与拷贝赋值的主要数据是通过不断的复制拷贝传递,移动构造与移动赋值的主要数据是通过不断的击鼓传花传递。
有一些地方说右值引用延长了数据的声明周期,这是不完全准确的,从上面的讲解可以看到,右值引用是通过不断的转移资源,来确保了数据的保留。并未干扰到析构的时机。
解决传值插入问题
C++11将STL库中容器的插入也进行了更新,此处举例几个容器:
list容器的push_back相关文档
vector容器的pusk_back相关文档
int main()
{
cr::string s1("hello");
cout << "--------------- vector -------------------" << endl;
vector<cr::string> v;
v.push_back(s1);
cout << "===================================" << endl;
v.push_back(cr::string("world"));
cout << "----------------- list -------------------" << endl;
list<cr::string> lt;
lt.push_back(s1);
cout << "===================================" << endl;
lt.push_back(cr::string("world"));
return 0;
}
vector容器与list容器的push_back右值引用版,有些许的不同是因为其内部,源代码的不同所导致的。但是其本质上都是有一次资源移动(移动构造)。
以list容器的push_back做分析:
int main()
{
list<cr::string> v;
cr::string s1("1111");
v.push_back(s1);
v.push_back("2222");
v.push_back(std::move(s1));
return 0;
}
左值:把对象构造上去。由于左值不能也不敢转移资源,移动资源。所以左值构造就是拷贝构造。
右值:把对象构造上去。由于右值是将亡值,所以没有必要调用拷贝构造,需要调用移动构造。
所以,对于插入接口都提供了右值引用接口:
STL容器,插入接口C++11后都提供右值版本。插入过程中,如果传递对象是右值对象,那么进行资源转移减少拷贝。
vector容器的相关文档
list容器的相关文档
完美转发
模板中的&&万能引用
template<typename T>
void PerfectForward(T&& t) // 引用折叠/万能引用
{
Fun(t);
}
如果是普通参数的这个版本叫做右值版本,但是如果模板之后提供这个版本,可以叫做万能引用,也可以叫做引用折叠。
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; }
// 万能引用:T既能引用左值,也能引用右值
// 引用折叠:a.左值&&会被折叠为&;b.右值会被右值引用然后变为左值,折叠为左值
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); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
左值引用后是左值。右值引用后变为左值。通通变为左值
右值经过右值引用变为左值,正是前面所提的了解的知识点(需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,编译器会为其开辟一个空间,然后将10存进去,再引用。也就是说,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,实际中右值引用的使用场景并不在于此,这个特性也不重要。(作为了解))
可以理解为,编译器是为了实现一些底层所导致的。至于那些底层,过复杂,没有必要了解研究。
问:那我们这么保持属性呢?
答:利用完美转发std::forward。
完美转发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)
{
// 完美转发:保持t引用对象属性
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
是左值就是左值,是右值就是右值。不再会折叠掉属性。
完美转发实际中的使用场景
(利用list容器的部分模拟实现以及string容器的部分模拟实现讲解)
当未使用完美转发的时候:
namespace cr
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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 << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
namespace cr
{
// List容器节点
template<class T>
struct ListNode
{
ListNode<T>* _next = nullptr;
ListNode<T>* _prev = nullptr;
T _data;
ListNode(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{}
};
// List容器
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, x);
}
void PushBack(const T& x)
{
Insert(_head, x);
}
// 当不使用完美转发的时候
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node(x);
// prev newnode pos的连接
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(x);
// prev newnode pos的连接
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
}
int main()
{
cr::List<cr::string> lt;
cout << "----------------------------------" << endl;
lt.PushBack("world");
return 0;
}
由于,引用折叠,左值&&会被折叠为&,右值会被右值引用然后变为左值。所以根本下一步不会右值,左值与右值在前一步皆退化了,变为左值。
当使用完美转发的时候:
代码改变的部分:
// 当使用完美转发的时候
void PushBack(T&& x)
{
Insert(_head, std::forward<T>(x));
}
// 当使用完美转发的时候
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node(std::forward<T>(x));
// prev newnode pos的连接
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
增加后的运行结果:
增加这么多是因为其是一个 “环” :
第一步:
第二步:
第三步:
note:
需要使用完美转发std::forward,否者因为:引用折叠,左值&&会被折叠为&,右值会被引用然后变为左值。最终还是调用一次拷贝构造。