一.右值引用
C++11增加了一个新的类型,称为右值引用(R-value reference),标记为T &&。右值是指表达式结束后就不再存在的临时对象。相对应的左值就是指表达式结束后依然存在的持久对象,所有的具名变量或对象都是左值,而右值不具名。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值。
在C++11中,右值由两个概念构成,一个是将亡值(xvalue,expiring value)(C++11新增的,与右值引用相关的表达式,比如将要被移动的对象,T&& 函数返回值,std::move返回值和转换为T&&的类型的转换函数的返回值),另一个是纯右值(rvalue, PureRvalue)(非引用返回的临时变量,运算表达式产生的临时言火日王,原始字面量和lambda表达式等都是纯右值。C++11中所有的值必属于左值,将亡值,纯右值三者之一。
1.&&的特性
与左值引用相类似,右值引用就是对右值进行引用的类型。因为右值不具名,所以,我们只能通过引用的方式找到它。
1).声明右值引用时必须立即进行初始化。因为引用类型并不拥有所绑定对象的别名,只是该对象的一个别名。
2).通过右值引用的声明,该右值的生命周期将会与右值引用类型变量的生命周期一样,只要变量还在,右值就会一直存活。
#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;
}
在关闭返回值优化的情况下,输出结果:
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
拷贝构造函数调用了两次:
1).GetA()函数内部创建的对象返回后构造一个临时对象时调用。
2).在main函数中构造a对象时调用。
优化(右值引用绑定了右值,让临时右值的生命周期延长了):
int main(){
A&& a = GetA();
return 0;
}
//输出结果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
在C++98/03中,通过
常量左值引用也经常用来做性能优化,输出结果与右值引用一样。因为常量左值引用是一个“万能”的引用类型,可以接受左值,右值,常量左值和常量右值。
实际上,T&&并不是一定表示右值,它绑定的类型是未定的,即可能是左值又可能是右值。
template <typename T>
void f(T&& param);
f(10); //param是右值
int x = 10;
f(x); //param是左值
template<typename T>
void f(T&& param); //universal references
template<typename T>
class Test{
...
Test(Test&& hrs); //右值引用
...
};
void f(Test&& param); //右值引用
template <typename T>
void f (std::vector<T>&& param); //右值引用
temple <typename T>
void f(const T&& param); //右值引用
记住,如果不是universal references,用一个左值初始化一个右值引用类型是不合法的。
正确的做法是使用std::move将一个左值转换成右值。
int w1;
decltype(w1)&& v1 = w1; //error
decltype(w1)&& v1 = std::move(w2);
编译器会将己命名的右值引用视为左值,而将未命名的右值引用视为右值。
void PrintValue(int& i){
std::cout<<"lvalue : "<<i<<std::endl;
}
void PrintValue(int&& i){
std::cout<<"rvalue : "<<i<<std::endl;
}
void Forward(int&& i){
PrintValue(i);
}
int main(){
int i=0;
PrintValue(i);
PrintValue(1);
Forward(2);
}
输出结果:
lvalue : 0
rvalue : 1
lvalue : 2
2.右值引用优化性能,避免深拷贝(C++11加入右值引用的原因)
对于含有堆内存的类,我们都需要提供其深拷贝的构造函数,否则,会使用其默认提供的拷贝构造函数,容易导致堆内存的重复删除,指针指向为空。
class A
{
public:
A():m_ptr(new int(0)){}
~A(){
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); //临时变量的m_ptr指向为空,析构时,重复删除引起错误...
}
下面是正确的做法,提供了深拷贝的拷贝构造函数
class A
{
public:
A() : m_ptr(new int(0)){
cout<<"constructor"<<endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout<<"copy construct"<<endl;
}
~A(){
cout<<"destruct"<<endl;
delete m_ptr;
}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
输出结果:
construct
construct
copy construct
destruct
destruct
destruct
这样虽然是安全的,但是却因为拷贝构造带来了额外的损耗。
Get函数会返回临时变量,然后通过临时变量拷贝构造一个新的对象b,临时变量在拷贝构造完成之后销毁了,如果堆内存很大,那么这个拷贝构造的代价会很大。因为可以使用移动构造函数(对右值引用进行浅拷贝)。
class A
{
public:
A():m_ptr(new int(0)){
cout<<"construct"<<endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout<<"copy construct"<<endl;
}
A(A&& a) : m_ptr(a.m_ptr){
a.m_ptr = nullptr;
cout<<"move construct: "<<endl;
}
~A(){
cout<<"destruct"<<endl;
delete m_ptr;
}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
//输出结果
construct
construct
move construct
destruct
destruct
destruct
移动构造函数中,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。右值引用的一个重要的目的是用来支持移动语义的。
下面看一个MyString类实现的例子
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._data);
}
std::cout<<"Copy Assignment is called! source: "<<str.m_data<<std::endl;
return *this;
}
virtual ~MyString(){
if(m_data)
delete[] m_data;
}
};
int main(){
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
return 0;
}
//MyString的移动构造函数和移动赋值函数
MyString(MyString&& str){
std::cout<<"Move Constructor is called! source: "<<str._data<<std::endl;
_len=str._len;
_data=str._data;
str._len=0;
str._data=NULL;
}
MyString& operator=(MyString&& str){
std::cout<<"Move Assignment is called! source: "<<str._data<<std::endl;
if(this!=&str){
_len=str._len;
_data=str._data;
str._len=0;
str._data=NULL;
}
return *this;
}
3.move语义
移动语义是通过右值引用来匹配临时值的,普通的左值该怎么办呢?C++11提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象的内存或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝,将一个左值强制转换为一个右值引用。
std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);
使用了move几乎没有任何代价,只是转换了资源的所有权。实际上是将左值变成右值引用,然后应用move语义调用构造函数,就避免了拷贝,提高了性能。
4.forward和完美转发
一个右值引用参数作为函数的形参,在函数内部再转发该参数时变成了一个左值,不是原来的类型了。
template <typename T>
void forwardValue(T& val){
processValue(val); //右值参数会变成左值
}
template <typename T>
void forwardValue(const T& val){
processValue(val); //参数都变成常量左值引用了
}
因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发称为完美转发。C++11提供了一个函数std::forward,为转发而生,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,都会按照参数本来的类型转发。
void Print(int& t){
cout<<"lvalue"<<endl;
}
template <typename T>
void PrintT(int &t){
cout<<"rvalue"<<endl;
}
template <typename T>
void TestForward(t && v){
PrintT(v);
PrintT(std::forward<T>(v));
PrintT(std::move(v));
}
Test(){
TestForward(1);
int x=1;
TestForward(x);
TestForward(std::forward<int>(x));
}
//输出结果:
lvalue
rvalue
rvalue
5.emplace_back减少内存拷贝和移动
emplace_back能就地通过参数构造对象,不需要拷贝或者移动内存,相比push_back能更好地避免内存的拷贝和移动,使容器查入元素的性能得到进一步提升。在大多数情况下应该优先使用emplace_back来代替push_back。所有的标准库容器(array除外,因为它长度不可以变,不能插入元素)都增加了类似的方法:emplace,emplace_hint,emplace_front,emplace_after和emplace_back。
#include <vector>
#include <iostream>
using namespace std;
struct A
{
int x;
double y;
A(int a, double b):x(a),y(b){}
};
int main(){
vector<A> v;
v.emplace_back(1,2);
cout<<v.size()<<endl;
return 0;
}
emplace_back的用法比较简单,直接通过构造函数的参数就可以构造对象,因此,也要求对象必须有对应的构造函数。如果没有,编译器会报错。
#include <vector>
#include <map>
#include <string>
#include <iostream>
using namespace std;
struct Complicated
{
int year;
double country;
std::string name;
Complicated(int a, double b, string c):year(a),country(b),name(c){
cout<<"is constucted"<<endl;
}
Complicated(const Complicated& other):year(other.year),country(other.country),name(std::move(other.name)){
cout<<"is moved"<<endl;
}
};
int main(){
std::map<int, Complicated> m;
int anInt = 4;
double aDouble = 5.0;
std::string aString = "C++"
cout<<"--insert--"<<endl;
m.insert(std::make_pair(4,Complicated(anInt, aDouble, aString)));
cout<<"--emplace--"<<endl;
m.emplace(4, Complicated(anInt, aDouble, aString));
cout<<"--emplace_back--"<<endl;
vector<Complicated> v;
v.emplace_back(anInt, aDouble, aString);
cout<<"--push_back--"<<endl;
v.push_back(Complicated(anInt, aDouble, aString));
return 0;
}
//输出结果:
--insert--
is constructed
is moved
is moved
--emplace--
is constructed
is moved
--emplace_back--
is constructed
--push_back--
is constructed
is moved
is moved