目录
前言
C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
一、什么是左值、右值?
可以从2个角度判断:
- 左值可以取地址、位于等号左边;
- 而右值没法取地址,位于等号右边。
// 案例 1int a = 6 ;
a
可以通过
&
取地址,位于等号左边,所以
a
是左值。
6
位于等号右边,
6
没法通过
&
取地址,所以
6
是个右值。
再举个复杂点的例子:
// 案例 2struct A {A ( int a = 0 ) {a_ = a ;}int a_ ;};A a = A ();
同样的,
a
可以通过
&
取地址,位于等号左边,
所以
a
是左值
。
A()
是个临时值,没法通过
&
取地址,位于等号右边,
所以
A()
是个右值
。
可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。
二、什么是左值引用、右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。
1.左值引用
左值引用:能指向左值,不能指向右值的就是左值引用。
代码如下(示例):
// 示例 1int a = 5 ;int & ref_a = a ; // 左值引用指向左值,编译通过int & ref_a = 5 ; // 左值引用指向了右值,会编译失败// 示例 2 ( const左值引用是可以指向右值的)const int & ref_a = 5 ; // 编译通过,const左值引用不会修改指向值// 实例3 ( const & 作为函数参数)void push_back ( const value_type & val ); // 编译通过,const左值引用不会修改指向值
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
2.右值引用
再看下右值引用,右值引用的标志是
&&
,顾名思义,右值引用专门为右值而生,
可以指向右值,不能指
向左值
:
代码如下(示例):
int && ref_a_right = 5 ; // okint a = 5 ;int && ref_a_left = a ; // 编译不过,右值引用不可以指向左值ref_a_right = 6 ; // 右值引用的用途:可以修改右值
3 对左右值引用本质的讨论
左右值引用的本质。
3.1 右值引用有办法指向左值吗?
有办法,使用
std::move
:
代码如下(示例):
int a = 5 ; // a 是个左值int & ref_a_left = a ; // 左值引用指向左值int && ref_a_right = std::move ( a ); // 通过 std::move 将左值转化为右值,可以被右值引用指向cout << a ; // 打印结果: 5
- 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
- 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。
同样的,
右值引用能指向右值
,本质上也是把右值提升为一个左值,并定义一个右值引用通过
std::move 指向该左值:
int && ref_a = 5 ;ref_a = 6 ;等同于以下代码:int temp = 5 ;int && ref_a = std::move ( temp ); // 此时 temp 等于右值ref_a = 6 ;
3.2 左值引用、右值引用本身是左值还是右值?
- 被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码:
// 形参是个右值引用
void change ( int && right_value ) {right_value = 8 ;}int main () {int a = 5 ; // a 是个左值int & ref_a_left = a ; // ref_a_left 是个左值引用int && ref_a_right = std::move ( a ); // ref_a_right 是个右值引用change ( a ); // 编译不过, a 是左值, change 参数要求右值change ( ref_a_left ); // 编译不过,左值引用 ref_a_left 本身也是个左值change ( ref_a_right ); // 编译不过,右值引用 ref_a_right 本身也是个左值change ( std::move ( a )); // 编译通过change ( std::move ( ref_a_right )); // 编译通过change ( std::move ( ref_a_left )); // 编译通过change ( 5 ); // 当然可以直接接右值,编译通过// 打印下面三个左值的地址,都是一样的cout << & a << ' ' ;cout << & ref_a_left << ' ' ;cout << & ref_a_right ;}
- 作为函数返回值的 && 是右值,直接声明出来的 && 是左值
结论
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
4、右值引用和std::move使用场景
std::move 只是类型转换工具,不会对性能有好处;
右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗?
4.1 func(const T&)优化性能,避免浅拷贝
浅拷贝重复释放
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 运行报错
}
cout << "main finish" << endl;
return 0;
}
// 上面代码运行结果
constructor Aconstructor Aready returndestructor A, m_ptr: 0xf87af8destructor A, m_ptr:0xf87ae8destructor A, m_ptr: 0xf87af8main finish
深拷贝构造函数
在上面的代码中,默认构造函数是浅拷贝,main
函数的
a
和
Get
函数的
b
会指向同一个指针
m_ptr
,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
// 上面代码 运行结果constructor Aconstructor Aready returncopy constructor Adestructor A, m_ptr:0xea7af8destructor A, m_ptr:0xea7ae8destructor A, m_ptr:0xea7b08main finish
移动构造函数
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
// 移动构造函数,可以浅拷贝
A(A&& a) :m_ptr(a.m_ptr) {
a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
cout << "move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
if(m_ptr)
delete m_ptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
// 上面代码 运行结果constructor Aconstructor Aready returnmove constructor Adestructor A, m_ptr:0destructor A, m_ptr:0xfa7ae8destructor A, m_ptr:0xfa7af8main finish
上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct
)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&
,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&&
用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move
语义),
右值引用的一个重
要目的是用来支持移动语义的。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少
不必要的临时对象的创建
、拷贝以及销毁,可以大幅度提高
C++
应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
4.2 移动(move )语义
move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要
move
语义起作用,核心在于需要对应类型的构造函数支持。
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString() {
m_data = NULL;
m_len = 0;
}
MyString(const char* p) {
m_len = strlen (p);
copy_data(p);
}
MyString(const MyString& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data <<
std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data <<
std::endl;
return *this;
}
// 用c++11的右值引用来定义这两个函数
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str.m_data <<
std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str.m_data <<
std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
virtual ~MyString() {
if (m_data) free(m_data);
}
};
int main()
{
MyString a;
a = MyString("Hello"); // Move Assignment
MyString b = a; // Copy Constructor
MyString c = std::move(a); // Move Constructor is called! 将左值转为右值
std::vector<MyString> vec;
vec.push_back(MyString("World")); // Move Constructor is called!
return 0;
}
有了func(const T&)引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。
4.3 forward 完美转发
forward 完美转发实现了
参数在传递过程中保持其值属性的功能
,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
现存在一个函数
Template < class T >void func ( T && val );
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,
这个
val
值它本质上是一个左值!
看下面例子
int && a = 10 ;int && b = a ; // 错误int && b = std::forward < int > ( a ); //正确
4.4 emplace_back 减少内存拷贝和移动
对于STL容器,C++11后引入了emplace_back接口,emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。
考虑这样的语句:
vector < string > testVec ;testVec . push_back ( string ( 16 , 'a' ));
上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现:
- 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
- 其次,vector内会创建一个新的string对象,这是第二次构造。
- 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次 string构造和一次析构。
c++11
可以用
emplace_back
代替
push_back
,
emplace_back
可以直接在
vector
中构建一个对象,而非创建一个临时对象,再放进vector
,再销毁。
emplace_back
可以省略一次构建和一次析构,从而达到优化的目的。
总结
C++11
在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、
forward
、
emplace
和一些无序容器我们可以大幅度改进程序性能。
右值引用仅仅是通过改变资源的所有者来避免内存的拷贝,能大幅度提高性能。
forward
能根据参数的实际类型转发给正确的函数。
emplace
系列函数通过直接构造对象的方式避免了内存的拷贝和移动。