C++11中引入右值引用和移动语义,可以避免无谓的复制,提高程序性能。
2.1 右值引用
C++11新增类型,右值引用,标记为T &&。左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值和右值的方法是能不能对表达式取地址,如果能,则为左值,如果不能,则为右值。所有具名变量都是左值。
C++11中右值有将亡值和纯右值。将亡值是与右值引用有关的表达式,如要被移动的对象,T&&函数返回值,std::move返回值和转换为T&&的类型的转换函数的返回值。纯右值包括非引用返回的临时变量,运算表达式产生的临时变量,原始字面量和lambda表达式等。
// i是左值,0是右值(字面量)
int i = 0;
2.1.1 &&的特性
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以只能通过引用的方式找到它。
无论声明左值引用还是右值引用,都必须立即进行初始化。通过右值引用的声明,其生命周期与右值引用变量的生命周期一样,只要该变量还活着,该右值临时量将会存活下去。
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;
}
为了清楚观察结果,gcc编译时设置编译选项-fno-elide-constructors来关闭返回值优化。输出结果:
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
拷贝构造函数调用了两次,一次是GetA() return A(); 一次是main A a=;
通过右值引用来延长临时右值的声明周期:
int main() {
A&& a = GetA();
return 0;
}
输出结果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
通过右值引用,比之前减少了一次拷贝构造和一次析构,原因是右值引用让临时右值的声明周期延长了。我们可以利用这个做性能优化,即避免临时对象的拷贝构造和析构。
事实上,C++03通过常量左值引用也可以做性能优化:
const A& a = GetA();
常量的左值引用是一个"万能"的引用类型,可以接受左值,右值,常量左值和常量右值。
注意,普通左值引用不能接受右值:
// error
A& a = GetA();
注意,T&&并不一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。例子如下:
template<typename T>
void f(T&& param);
f(10); //10右值
int x = 10;
f(x); //x左值
2.1.2 右值引用优化性能,避免深拷贝
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,运行错误。
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);
}
上面的代码,默认构造函数是浅拷贝,a和b会指向同一个指针m_ptr,析构的时候会导致重复删除,运行出错。正确的做法是提供深拷贝的构造函数:
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()
{
cout << "destruct " << 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);
}
上面的代码输出:
construct //Get: A b
construct //Get: return b
copy construct //main: A a =
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;
};
A Get(bool flag)
{
A a;
A b;
if (flag)
return a;
else
return b;
}
int main()
{
A a = Get(false);
}
上面的代码将输出:
construct //Get: A b
construct //Get: return b
move construct //main: A a =
destruct
destruct
destruct
移动语义可以将资源通过浅拷贝的方式从一个对象转移到另一个对象,这样能够减少不必要临时对象的创建,拷贝和销毁。需要注意的是,我们一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。