文章目录
1. 左值和右值
1.1 左值
左值是指可以出现在赋值运算符左边的表达式,它表示一个具有明确存储地址(可以是栈、堆等)和较长生命周期的对象
特点:
- 左值既可以出现在赋值运算符
=
的左边,也可以出现在=
的右边 - 可以对左值取地址
- 可以修改左值(被
const
修饰的左值除外) - 具有明确存储地址(可以是栈、堆等)和较长生命周期的对象
例如,以下都是左值:
int main()
{
int a = 1;
int* p = &a;
const int b = 0;
return 0;
}
1.2 右值
右值是指只能出现在赋值运算符右边的表达式,它表示一个临时的、即将被销毁的对象。右值通常是一个字面量、临时对象或者函数返回的临时结果。
特点:
- 右值只能出现在
=
的右边 - 一般不能对右值取地址,因为它并没有被实际存储
- 右值不能被修改
例如,以下都是右值
int main()
{
"abcd";
1;
double a = 1 + 1; //中间产生的临时变量也是一个右值
std::min(1, 2); //该函数的返回值会生成一个临时变量,是一个右值
return 0;
}
注:如果一个函数的返回值是一个左值引用,那么该返回值就是是一个左值,而不是右值。例如:
class String
{
public:
std::string& getString() { return _str; }
private:
std::string _str;
};
2. 左值引用和右值引用
左值引用和右值引用本质上都是取别名
引用的意义就是为了减少拷贝
2.1 左值引用
左值引用就是对左值的引用,表示给左值取别名,用
&
声明
例如:
int main()
{
int a = 1;
int* p = &a;
const int b = 0;
int& ra = a;
int*& rp = p;
const int& rb = b;
return 0;
}
2.2 右值引用
右值引用就是对右值的引用,表示给右值取别名,用
&&
声明
注:
- 如果给一个常量进行右值引用,那么就会延长该常量的生命周期,编译器会在栈上为这个临时对象分配一块内存,然后让这个右值引用使用这块内存。
- 因为编译器会在栈上为这个临时对象分配一块内存,所以,我么可以对右值引用取地址,取到的就是这块栈上的内存
- 甚至,我们也可以对右值引用取地址后解引用,对这块内存进行修改(注意,改的是内存的内容,而不是引用的常量)
例如:
int main()
{
int&& ra = 10;
int* p = &ra;
*p = 1;
std::cout << ra;
return 0;
}
output:
1
如果我们不想让右值被修改,可以在前面加上const
修饰
const int&& ra = 10;
2.3 左值引用 引用 右值
左值引用是否可以引用右值,例如:
double& a = 10;
不能,因为这涉及到引用权限放大 ,10
是一个常量,不能被修改,而左值引用引用的是可以被修改的左值,这显然是不行的
如果要让左值引用引用右值,需要在前面加上**const
**修饰,表示该左值引用不可修改,这样就不涉及权限放大的问题了
const double& a = 10;
2.4 右值引用 引用 左值
右值引用是否可以引用左值,例如:
double x = 1;
double&& a = x;
不能,右值引用只能引用右值,不能引用左值。
但是,对左值使用std::move
后,就可以用右值引用了
double&& a = std::move(x);
2.4.1 std::move
std::move
的主要作用为:将左值转换为右值引用
其本质是通过函数模板,进行类型的转换
namespace std {
template<typename T>
typename remove_reference<T>::type&& move(T&& param) {
return static_cast<typename remove_reference<T>::type&&>(param);
}
}
3. 右值引用的意义
要讨论右值引用的意义,我们首先需要清楚左值引用及解决了哪些问题
左值引用解决了传参时拷贝的问题
左值引用解决了部分返回值拷贝的问题
- 如果出了作用域,返回值还存在,就可以用左值引用防止拷贝
- 如果出了作用域,返回值会被销毁(局部对象),那就不能用左值引用了
例如在string
类的模拟实现中:
//赋值运算符重载(现代写法)
string& operator=(const string& s)
{
string tmp(s); //用s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
将参数设置为左值引用,并将不会被销毁的返回值对象也设置为左值引用,就可以避免拷贝构造,从而提高效率
但是,如果是下面的函数:
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 += (x + '0');
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
此时的返回值str
是一个临时对象,出了作用域就会被销毁,因此返回值类型不能是左值引用。此时就无法避免临时对象拷贝构造一个新的****string
****类对象的问题了
例如:
string a = to_string(13);
上面的代码理论上就会产生两次拷贝构造,但是编译器会做优化,变成一次,如图:
那么为了解决 出了作用域,返回值会被销毁(局部对象),那就不能用左值引用了 这一问题,就需要右值引用
但是:
我们又不能简单的将函数的返回值改为右值引用,因为返回的
str
是一个左值string&& to_string(int value) { //..... return str; }
也不能同时将
str
通过std::move()
强制转换为一个右值引用,因为str
出了作用域后仍然会被销毁string&& to_string(int value) { //..... return std::move(str); }
3.1 移动构造
为了清楚右值引用是如何解决拷贝的问题,我们需要了解一个概念:将亡值
实际上C++11将右值分为了两类:
- 纯右值:即内置类型的右值,如
abcd
,10
- 将亡值:自定义类型的右值,即那些处于生命周期末尾,但仍然拥有可以被利用资源的对象。
例如,string a = to_string(13)
- 在
to_string()
函数返回后,会在栈区创建一个临时对象,这个临时对象在拷贝构造完对象a
后,就会被销毁 - 但是为了防止新对象和就对象指向的是同一份资源,我们目前实现的拷贝构造是深拷贝
- 但是,这个临时对象是一个将亡值,使用完后就会被销毁,那它为什么还需要保留数据呢?
- 所以,如果拷贝构造时的
source
是一个将亡值,那么我们就不需要进行深拷贝,而是可以直接将它的资源掠夺过来,从而提高效率
这就是使用右值引用的移动构造函数,例如:
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s); //直接将将亡值s的资源进行转移
}
此时,对于同样的代码string a = to_string(13)
,理论上就只会进行一次拷贝构造和一次移动构造了,而编译器又会进行优化,只进行一个移动构造,如图:
移动构造和拷贝构造的区别:
- 在没有增加移动构造之前,由于拷贝构造采用的是const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
- 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则) 。
- string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小。
3.2 移动赋值
如果代码是这样的:
string a;
a = to_string(13);
那么这个过程中就会产生一次拷贝构造和一次拷贝赋值,并且编译器不会优化拷贝次数,但是会和上面一样,将返回的str
识别成将亡值,如图:
那么,在后面的拷贝赋值的过程中,我没有会将将亡值深拷贝给a
,这次深拷贝同样是没有必要的
因此,和移动构造一样,我们还需要一个移动赋值
string& operator=(string&& s)
{
swap(s); //直接将将亡值s的资源转移给自己
return *this;
}
这样,拷贝赋值就会变成移动赋值,从而减少了一次深拷贝,提高了效率
3.3 STL中右值引用版本的插入函数
C++11标准给容器都添加了参数为右值引用的插入函数
例如list
:
std::list::push_back
void push_back (const value_type& val); //左值引用版本
void push_back (value_type&& val); //右值引用版本
例如:
int main()
{
list<string> _list;
string str = "abcde";
_list.push_back(str); //str为左值,使用左值引用版本
_list.push_back("abcde"); //会用"abcde"构造一个临时的string对象,使用右值引用版本
_list.push_back(string("abcde")); //string("abcde")为匿名对象,将亡值,使用右值引用版本
_list.push_back(move(str)); //move后为右值,使用右值引用版本
return 0;
}
4. 完美转发
4.1 万能引用(引用折叠)
万能引用是一种特殊的引用类型,它既可以绑定到左值,也可以绑定到右值。
万能引用的语法形式是通过模板参数和右值引用语法实现的,具体表现为
T&&
,其中T
是一个模板类型参数。
- 如果传入的是一个左值那么 && -> &
- 如果传入的是一个右值那么 && -> &&
- 即传左值,就是左值引用,传右值,就是右值引用;传const就是const
例如:
template <typename T>
void perfectForward(T&& t) //这里的T&&就不是一个右值引用,而是一个万能引用
{
//....
}
注意:
只有在模板类型推导的上下文中,
T&&
才是万能引用。如果T
是一个具体的类型,那么T&&
就是普通的右值引用例如:
template<typename T> class MyClass { public: // 这里的 T 是模板类的模板参数,在类实例化时确定 void func(T&& arg) { std::cout << "Called with rvalue reference" << std::endl; } };
上面的代码中,
void func(T&& arg)
中的T&&
不是万能引用,而是一个右值引用。因为在实例化类对象的时候,
T
就已经确定了。而不需要在调用的时候进行推导
我们来看下面的代码:
void func(int&) { cout << "左值引用" << endl; }
void func(int&&) { cout << "右值引用" << endl; }
void func(const int&) { cout << "const 左值引用" << endl; }
void func(const int&&) { cout << "const 右值引用" << endl; }
template <typename T>
void perfectForward(T&& t) { func(t); }
int main()
{
int a = 1;
const int b = a;
perfectForward(a); //左值
perfectForward(move(a)); //右值
perfectForward(b); //const左值
perfectForward(move(b)); //const右值
return 0;
}
output:
左值引用
左值引用
const 左值引用
const 左值引用
在上面的代码中,我们用模板参数T的万能引用接收不同类型的数据,就是希望它能将不同类型的数据正确匹配到不同的重载函数
但很明显,结果是错的。右值引用被t
接收后,被识别成了左值;const
右值引用被t
接收后,也被识别成了const
左值
4.2 右值引用的属性
为了弄清这一点,我们需要知道一个结论:
右值被右值引用后,这个右值引用的属性是左值
例如:
int main() { int&& a = 10; ++a; //可以修改右值引用,说明右值引用的属性是左值 return 0; }
为什么要这样设计?我们可以这样理解:
- 右值(将亡值)是不能被改变的,但是为了实现移动语义,能够将
将亡值
的资源转移给另外的对象,我们又需要对这个右值进行修改 - 为了实现这一点,我们就可以让一个右值引用指向这个将亡值,并将右值引用的属性设计为左值,可以对其进行修改。这样就可以完成移动语义了
所以,在上面的代码中:
template <typename T>
void perfectForward(T&& t) { func(t); }
一个右值被t
接收后,t
就是一个右值引用,但是右值引用的属性是左值,因此它就会被传给形参为左值引用的func
函数,从而导致结果错误
4.3 完美转发 std::forward
为了解决上面的问题,就需要用到完美转发:
lvalue (1) template <class T> T&& forward (typename remove_reference<T>::type& arg) noexcept;
rvalue (2) template <class T> T&& forward (typename remove_reference<T>::type&& arg) noexcept;
函数std::forward
能够保持参数原有的属性,例如
void func(int&) { cout << "左值引用" << endl; }
void func(int&&) { cout << "右值引用" << endl; }
void func(const int&) { cout << "const 左值引用" << endl; }
void func(const int&&) { cout << "const 右值引用" << endl; }
template <typename T>
void perfectForward(T&& t) { func(std::forward<T>(t)); }
int main()
{
int a = 1;
const int b = a;
perfectForward(a); //左值
perfectForward(move(a)); //右值
perfectForward(b); //const左值
perfectForward(move(b)); //const右值
return 0;
}
output
左值引用
右值引用
const 左值引用
const 右值引用
注意:
要想保持右值的属性,在每次右值传参时都需要进行完美转发