C++11增加了一个新的类型,称为右值引用,标记为T&&。
左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。
一个区分左值和右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。所有的具名变量或对象都是左值,而右值不具名。
C++11中,右值由两个概念构成,一个是将亡值,另一个是纯右值,比如,非引用返回的临时变量,运算表达式产生的临时变量,原始字面量和lambda表达式都是纯右值。而将亡值是C++11新增的,与右值引用相关的表达式,比如,将要被移动的对象,T&&函数返回值,std::move返回值和转换为T&&的类型的转换函数的返回值。
C++11中所有的值必属于左值,将亡值,纯右值三者之一,将亡值和纯右值都属于右值。区分表达式的左右值属性有一个简便方法:若可对表达式用&符取址,则为左值,否则为右值。
比如,简单的赋值语句:
int i = 0;
在这条语句中,i是左值,0是字面量,就是右值。在上面的代码中,i可以被引用,0就不可以了。字面量都是右值。
&&的特性
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因此引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只有该变量还活着,该右值临时量将会一直存活下去。
看一下下面代码:
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyconstructCount = 0;
int g_destructCount = 0;
struct A
{
A()
{
cout << "construct: " << ++g_constructCount << endl;
}
A(const A& a)
{
cout << "copy construct: " << ++g_copyconstructCount << endl;
}
~A()
{
cout << "destruct: " << ++g_destructCount << endl;
}
};
A getA()
{
return A();
}
int main()
{
A a = getA();
return 0;
}
为了清楚的观察临时值,在G++下编译时设置编译选项-fno-elide-constructors来关闭返回值优化效果。
输出结果:
从上面的例子中可以看到,在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是在getA()函数内部创建的对象返回后构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct是因为临时对象在构造a对象之后就销毁了。如果开启返回值优化,输出结果将是:
可以看到返回值优化将会把临时对象优化掉,但这不是C++标志,是各编译器的优化规则。我们在回到之前提到的可以通过右值引用来延长临时右值的生命周期,如果在上面的代码中通过右值引用来绑定函数返回值,结果就不一样了。在编译时设置编译选项-fno-elide-constructors。
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyconstructCount = 0;
int g_destructCount = 0;
struct A
{
A()
{
cout << "construct: " << ++g_constructCount << endl;
}
A(const A& a)
{
cout << "copy construct: " << ++g_copyconstructCount << endl;
}
~A()
{
cout << "destruct: " << ++g_destructCount << endl;
}
};
A getA()
{
return A();
}
int main()
{
A&& a = getA();
return 0;
}
输出结果:
通过右值引用,比之前少了一个拷贝构造和析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这一特点做一些优化,即避免临时对象的拷贝构造和析构。
实际上T&&并不是一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。
template<typename T>
void f(T&& param);
f(10); ///10是右值
int x = 10;
f(x); ///x是左值
从上面的代码可以看出,param有时是左值,有时是右值,因为在上面的例子中有&&,这表示param实际上是一个未定义的引用类型。这个未定的引用类型称为universal references,它必须被初始化,它是左值还是右值引用取决于它的初始化,如果&&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。
#include <iostream>
using namespace std;
void PrintValue(int& i)
{
cout << "lvalue: " << i << endl;
}
void PrintValue(int&& i)
{
cout << "rvalue: " << i << endl;
}
void Forward(int&& i)
{
PrintValue(i);
}
int main()
{
int i = 0;
PrintValue(i);
PrintValue(0);
Forward(2);
return 0;
}
Forward函数接受的是一个右值,但是转发给PrintValue时又变成了左值,因为在Forward中调用PrintValue时,右值i变成了一个命名对象,编译器会将其当做左值处理。
右值引用优化
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数会导致堆内存的重复删除,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A():m_ptr(new int(8))
{
cout << "construct: " << m_ptr << endl;
}
~A()
{
cout << "m_ptr: " << m_ptr << endl;
delete m_ptr;
}
private:
int* m_ptr;
};
A Get(bool flag)
{
A a;
A b;
if (flag)
{
return a;
}
else
{
return b;
}
}
int main()
{
{
A a = Get(false);
}
return 0;
}
在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一个指针m_ptr,在析构的时候会导致重复删除该指针,如下所示。
正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码(关闭返回值优化的情况下):
#include <iostream>
using namespace std;
class A
{
public:
A():m_ptr(new int(8))
{
len = 8;
cout << "construct m_ptr: " << m_ptr << endl;
}
///深拷贝
A(const A& a) : m_ptr(new int(a.len))
{
cout << "copy construct m_ptr: " << m_ptr << endl;
}
~A()
{
cout << "delete m_ptr: " << m_ptr << endl;
delete m_ptr;
}
private:
int* m_ptr;
int len;
};
A Get(bool flag)
{
A a;
A b;
if (flag)
{
return a;
}
else
{
return b;
}
}
int main()
{
A a = Get(false);
return 0;
}
上面的代码将输出:
这样做就可以保证拷贝构造时的安全性,但又是这种拷贝构造却是不必要的,比如上面的代码中的拷贝构造就是不必要的。上面代码中的Get函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?看下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
A():m_ptr(new int(8))
{
data = 0;
len = 8;
cout << "construct m_ptr: " << m_ptr << endl;
}
///深拷贝
A(const A& a) : m_ptr(new int(a.len))
{
cout << "copy construct m_ptr: " << m_ptr << endl;
}
A(A&& a):m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
data = 100;
cout << "move construct " << endl;
}
~A()
{
cout << "delete m_ptr: " << m_ptr << endl;
delete m_ptr;
}
int data;
private:
int* m_ptr;
int len;
};
A Get(bool flag)
{
A a;
A b;
if (flag)
{
return a;
}
else
{
return b;
}
}
int main()
{
A a = Get(false);
cout << "main func..." << endl;
cout << a.data <<endl;
return 0;
}
上面代码将输出:
上面的代码没有了拷贝构造,取而代之的是移动构造。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高了性能。这也就是所谓的移动语义,右值引用的一个重要目的是用来支持移动语义的。
移动语义可以将资源(堆,系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建,拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
下面代码实现了拷贝构造函数和拷贝赋值操作符。
#include <iostream>
#include <vector>
#include <string.h>
using namespace std;
class MyString
{
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);
cout << "Copy Construct is called... source: " << str.m_data << endl;
}
MyString& operator=(const MyString& str)
{
if (this != &str)
{
m_len = str.m_len;
copy_data(str.m_data);
}
cout << "Copy Assignment is called... source: " << str.m_data << endl;
return *this;
}
virtual ~MyString()
{
if (m_data)
{
delete [] m_data;
}
}
private:
void copy_data(const char* s)
{
m_data = new char[m_len + 1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
private:
char* m_data;
size_t m_len;
};
int main()
{
MyString a;
a = MyString("hello");
vector<MyString> v;
v.push_back(MyString("world"));
return 0;
}
实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString("hello")和MyString("world")都是临时对象,也就是右值。虽然它们都是临时的,但程序仍然调用了拷贝构造函数和拷贝赋值函数,造成了没有意义的资源申请和释放操作。如果能够直接使用临时对象已经申请的资源,既能节约资源,又能节省资源申请和释放的时间。这正是定义移动语义的目的。
用C++11的右值引用来定义这两个函数,代码如下所示。
#include <iostream>
#include <vector>
#include <string.h>
using namespace std;
class MyString
{
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);
cout << "Copy Construct is called... source: " << str.m_data << endl;
}
MyString& operator=(const MyString& str)
{
if (this != &str)
{
m_len = str.m_len;
copy_data(str.m_data);
}
cout << "Copy Assignment is called... source: " << str.m_data << endl;
return *this;
}
#if 1
MyString(MyString&& str)
{
cout << "Move Construct is called... source: " << str.m_data << endl;
m_len = str.m_len;
m_data = str.m_data;
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator==(MyString&& str)
{
cout << "Move Assignmeng is called... source: " << str.m_data <<endl;
if (this != &str)
{
m_len = str.m_len;
m_data = str.m_data;
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
#endif
virtual ~MyString()
{
if (m_data)
{
delete [] m_data;
}
}
private:
void copy_data(const char* s)
{
m_data = new char[m_len + 1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
private:
char* m_data;
size_t m_len;
};
int main()
{
MyString a;
a = MyString("hello");
vector<MyString> v;
v.push_back(MyString("world"));
return 0;
}
有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,我们一般在提供右值引用的构造函数时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以拷贝构造。