C++11 增加了一个新的类型,称为右值引用(R-value reference),标记为 T &&。在介绍右值引用类型之前先要了解什么是左值和右值。左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值 。所有的具名变量或对象都是左值,而右值不具名。
在 C++11 中,右值由两个概念构成,一个是将亡值(xvalue, expiring value),另一个则是纯右值(prvalue, PureRvalue),比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等都是纯右值。而将亡值是 C++11 新增的、与右值引用相关的表达式,比如,将要被移动的对象、 T&& 函数返回值、 std::move 返回值和转换为 T&&的类型的转换函数的返回值。
C++11 中所有的值必属于左值、将亡值、纯右值三者之一 ,将亡值和纯右值都属于右值。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
class A_2
{
public:
A_2() :m_ptr(new int(0))
{
}
~A_2()
{
delete m_ptr;
}
private:
int* m_ptr;
};
A_2 Get(bool flag)
{
A_2 a;
A_2 b;
if (flag)
return a;
else
return b;
}
int right_value_ref2()
{
A_2 a = Get(false); //运行报错
return 0;
}
在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一指针m_ptr,在析构函数会导致重复删除该指针。
正确的做法是提供深拷贝的拷贝构造函数。
A_2(const A_2& a):m_ptr(new int(*a.m_ptr)) //深拷贝
{
std::cout << "copy construct" << std::endl;
}
//这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,不人上面代码中的拷贝构造就是不必要的。上面代码中的Get函数会返回临时变量,
//然后通过这个临时变量拷贝构造一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。
//有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
class A_2
{
public:
A_2() :m_ptr(new int(0))
{
std::cout << "construct" << std::endl;
}
A_2(const A_2& a):m_ptr(new int(*a.m_ptr)) //深拷贝
{
std::cout << "copy construct" << std::endl;
}
A_2(A_2&& a) :m_ptr(a.m_ptr)
{
// 这一步很关键,真正实现资源拥有权的转移,如果去掉这一步,a.m_ptr在移动构造函数结束时候执行析构函数
// 会将我们偷来的内存析构掉。a.m_ptr会变成悬垂指针。如果我们对指针解引用,就会发生严重的运行错误
a.m_ptr = nullptr;
std::cout << "move construct:" << std::endl;
}
~A_2()
{
std::cout << "destruct" << std::endl;
delete m_ptr;
}
private:
int* m_ptr;
};
A_2 Get(bool flag)
{
A_2 a;
A_2 b;
if (flag)
return a;
else
return b;
}
int right_value_ref2()
{
A_2 a = Get(false);
return 0;
}
上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了临时对象的深拷贝,提供了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move 语义),右值引用的一个重要目的是用来支持移动语义的。值得注意的是移动构造里面将a.m_ptr置为nullptr(注释写得很清楚),此时delete不会对nullptr做出任何操作。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
再看一个简单的例子,代码如下:
struct Element
{
Element(){}
// 右值版本的拷贝构造函数
Element(Element&& other) : m_children(std::move(other.m_children)){}
Element(const Element& other) : m_children(other.m_children){}
private:
vector<ptree> m_children;
};
这个 Element 类提供了一个右值版本的构造函数。这个右值版本的构造函数的一个典型
应用场景如下:
void Test()
{
Element t1 = Init();
vector<Element> v;
v.push_back(t1);
v.push_back(std::move(t1));
}
先构造了一个临时对象 t1,这个对象中一个存放了很多 Element 对象,数量可能很多,如果直接将这个 t1 用 push_back 插入到 vector 中,没有右值版本的构造函数时,会引起大量的拷贝,这种拷贝会造成额外的严重的性能损耗。通过定义右值版本的构造函数以及
std::move(t1) 就可以避免这种额外的拷贝,从而大幅提高性能。
有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,我们一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。
这里也要注意对 move 语义的误解, move 只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于 move 语义,避免含有资源的对象发生无谓的拷贝。 move 对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如 int 和char[10] 数组等,如果使用 move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move 对于含资源的对象来说更有意义。
还要注意的是,右值引用不能绑定左值:int a; int &&c = a; 这样是不行的。
另附一段右值引用性能测试的代码:
#define MAX_TIMES 1000
class A {
public:
A(const char *pstr) {
m_data = (pstr != 0 ? strcpy(new char[strlen(pstr) + 1], pstr) : 0);
}
A(const A &a) {
m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
}
A &operator =(const A &a) {
if (this != &a) {
delete[] m_data;
m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
}
return *this;
}
A(A &&a) : m_data(a.m_data) {
a.m_data = 0;
}
A & operator = (A &&a) {
if (this != &a) {
m_data = a.m_data;
a.m_data = 0;
}
return *this;
}
~A() { delete[] m_data; }
private:
char * m_data;
};
//移动语义实现
void swap1(A& a, A& b)
{
A tmp(move(a));
a = (move(b));
b = move(tmp);
}
// 普通交换
void swap2(A& a, A& b)
{
A tmp = a;
a = b;
b = tmp;
}
int main()
{
A a("123"), b("456");
clock_t start, finish, start1, finish1;
double totaltime;
start = clock();
for (int i = 0; i < MAX_TIMES; ++i)
{
swap1(a, b);
}
finish = clock();
totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "MAX_TIMES =" << MAX_TIMES << "\n右值引用的运行时间为" << totaltime << "秒!" << endl;
start1 = clock();
for (int i = 0; i < MAX_TIMES; ++i)
{
swap2(a, b);
}
finish1 = clock();
totaltime = (double)(finish1 - start1) / CLOCKS_PER_SEC;
cout << "MAX_TIMES =" << MAX_TIMES << "\n普通的运行时间为" << totaltime << "秒!" << endl;
cin.get();
return 0;
}
测试结果:
10000次以下时几乎没有差别,这里不列出;
通过大量的测试表明,右值引用内比普通的构造性能确实要高,我这边测试大约是15倍的样子。