C++11 右值引用及性能优化
使用c++11有段时间了,但关于右值引用还是糊里糊涂的, 以至于无法应用其特性。记录此篇文章好好理理关于右值和右值引用。
1. 关于左值与右值
1.1 左值
左值可以说是最常见的,最简单的区分方法如下:
- 可以取地址
- 位于等号左边
int a = 0; //a是左值
1.2 右值
右值包括了两类,将亡值(xvalue)和纯右值,如非引用返回的临时变量、运算符产生的临时变量、原始字面量、lambda等,最简单的区分方法如下:
- 不可以取地址
- 位于等号右边
int a = 0; //0是右值
A a = A(); //A()是右值
2. 关于左值引用与右值引用
引用即是变量的别名,可以通过引用找到对应的值并修改,传参时使用引用可以避免临时变量的拷贝。
2.1 左值引用
左值引用只能指向左值
int a = 1; //左值
int& b = a; //左值引用,指向左值
int& c= 1; //左值引用,不能指向右值,无法编译
2.2 const 左值引用
const 左值引用是万能的引用,它可以指向右值,但不可以被修改
int a = 1; //左值
const int& b= a; //const左值引用,指向左值
const int& b= 1; //const左值引用,指向右值
由于const 左值引用可以接受右值,也常用于性能优化和特殊功能,如下:
- 函数传参接受常量,如果使用
int&则无法接受常量1
int sum(const int& a, const int& b)
{
return a+b;
}
sum(1, 2);
2.3 右值引用
右值引用只能指向右值 ,当右值是临时变量时,右值引用指向绑定的右后,临时变量将和右值引用同时存在,延长存活时间。
int a = 1; //左值
int&& b = 1;//右值引用,指向右值
int&& c = a;//右值引用,不能指向左值,无法编译
2.4 左值引用和右值引用判断函数
左值引用和右值引用可以使用以下函数来进行判断:
#include <type_traits>
int&& a = 1;
cout << is_lvalue_reference<decltype(a)>::value << endl; //输出false
cout << is_rvalue_reference<decltype(a)>::value << endl; //输出true
2.5 万能引用T&&和引用折叠
- 当发生自动类型推导时,&&将是一个未定的引用(universal references),例如模板T&&或auto&&,他是左值引用还是右值引用取决于初始化的值是左值还是右值,也就是说它既可以接受左值也可以接受右值
template <class T>
void sum(T&& a, T&& b)
{
cout << is_rvalue_reference<decltype(a)>::value << endl;
}
sum(1,4); // 此时T&&是右值引用,&& + && = &&
int a1= 2,a2 = 3;
sum(a1,a2);// 此时T&&是左值引用,& + && = &
- 当T&&有其他附加条件时将不再是 universal references,变成一个普通的右值引用
template <class T>
void sum(const T&& a, const T&& b) //被const 修饰,变成一个普通的右值引用
{
cout << is_rvalue_reference<decltype(a)>::value << endl;
}
template <class T>
void sum(vector<T>&& a, vector<T>&& b) //vector<T>是一个确定的类型,变成一个普通的右值引用
{
cout << is_rvalue_reference<decltype(a)>::value << endl;
}
- 当T&&最为参数被左值引用或右值引用初始化时,就会发生引用折叠,关于折叠引用的规则如下:
- 所有右值引用叠加到右值引用上仍然是一个右值引用
- 所有的其他引用叠加到右值引用上将变为左值引用
typedef int& TR;
typedef int&& TRR;
cout << is_rvalue_reference<TR>::value << endl; //左值引用
cout << is_rvalue_reference<TRR&&>::value << endl; //右值引用
cout << is_rvalue_reference<TRR&>::value << endl; //左值引用
2.6 左值引用和右值引用本身是左值还是右值
已命名的左值引用和右值引用都是左值,因为他在等号左边,可以取地址。而作为返回值的右值引用是右值。
void setA(int && a)
{
cout << is_rvalue_reference<decltype(a)>::value << endl;
}
int&& a= 1; //a是左值引用
setA(a); //无法编译,a是左值
2.7 std::move
std::move的作用是把左值转换为右值,以便于应用移动语义,也仅此而已。应用std::move后会转移资源控制所有权,当前他必须应用在实现了移动构造函数的对象上,像int、char*基础类型仍然会发生拷贝
int a= 1;
int&& b = std::move(a); //转换为右值
2.8 完美转发 std::forward。
由于右值引用是左值,当右值引用作为函数参数传递到内部函数时就会变成左值,如下:
void getData2(int && a)
{
}
template <class T>
void getData(T && a)
{
getData2(a); //无法编译,此时a是左值,无法传递给右值引用
}
getData(1);
这时就需要用到std::forward,他的作用也是左值右值的转换,但他既能转左值又能转右值,可以根据参数原来的类型转发到另一个函数上
void getData2(int && a){} //接受右值
template <class T>
void getData(T && a)
{
getData2(std::forward<T>(a)); //转发右值
}
getData(1);
void getData2(int & a){} //接受左值
template <class T>
void getData(T && a)
{
getData2(std::forward<T>(a)); //转发左值
}
int k = 1;
getData(k);
3. 性能优化
3.1 使用移动构造函数避免深拷贝
对于含有堆内存的类,一般需要深拷贝来确保拷贝构造和赋值构造时的安全性,不然会造成多次释放指针导致崩溃的问题,如下例子:
class CArrayData
{
public:
CArrayData(int iSize):m_iSize(iSize)
{
m_pData = new int [m_iSize];
for (int i = 0; i < m_iSize; i++)
{
m_pData[i] = m_iSize;
}
}
CArrayData(const CArrayData& arrayData)
{
copyDate(arrayData.m_iSize, arrayData.m_pData);
}
CArrayData& operator=(const CArrayData& arrayData)
{
copyDate(arrayData.m_iSize, arrayData.m_pData);
return *this;
}
~CArrayData()
{
delete[] m_pData;
}
private:
int m_iSize = 0;
int* m_pData = nullptr;
void copyDate(int iSize, int* pData)
{
delete[] m_pData;
m_iSize = iSize;
m_pData = new int [m_iSize];
for (int i = 0; i < m_iSize; i++)
{
m_pData[i] = pData[i];
}
}
};
CArrayData data(5); //调用构造函数
CArrayData data2 = data; //调用拷贝构造函数
CArrayData data3(3);
data3 = data2; //调用赋值构造函数
当堆内存很大时,深拷贝会有很大代价,这时可以使用右值引用来调用移动构造函数,通过修改arrayData.m_pData指针达到不被多次释放的目的,这是拷贝构造函数无法做到的,如下例子:
class CArrayData
{
public:
CArrayData(int iSize):m_iSize(iSize)
{
...
}
CArrayData(const CArrayData& arrayData)
{
...
}
CArrayData(CArrayData&& arrayData)//移动构造函数
{
m_iSize = arrayData.m_iSize;arrayData.m_iSize = 0;
m_pData = arrayData.m_pData;arrayData.m_pData = nullptr;
}
CArrayData& operator=(const CArrayData& arrayData)
{
...
}
CArrayData& operator=(CArrayData&& arrayData)//移动赋值函数
{
m_iSize = arrayData.m_iSize;arrayData.m_iSize = 0;
m_pData = arrayData.m_pData;arrayData.m_pData = nullptr;
return *this;
}
~CArrayData()
{
...
}
private:
int m_iSize = 0;
int* m_pData = nullptr;
...
};
CArrayData data(5); //调用构造函数
CArrayData data2 = std::move(data); //调用移动构造函数
CArrayData data3(3);
data3 = std::move(data2); //调用移动赋值函数
当然,赋值后data和data2转移了所有权,已经是空的数据了
以上两种方法的效率对比:
TimerElapse t1;
for (size_t i = 0; i < 10000; i++)
{
CArrayData data(10000); //调用构造函数
CArrayData data2 = data; //调用拷贝构造函数
}
cout << t1.elapse_micro() << endl; //耗时:585962 us
TimerElapse t1;
for (size_t i = 0; i < 10000; i++)
{
CArrayData data(10000); //调用构造函数
CArrayData data2 = std::move(data); //调用移动构造函数
}
cout << t1.elapse_micro() << endl;//耗时:249102 us
3.2 STL的性能优化
一般STL容器都实现了移动构造函数和移动赋值函数,可以直接使用,如下:
string str = "test";
vector<string> m_strArray;
m_strArray.push_back(str); //传入左值,使用拷贝
m_strArray.push_back(std::move(str)); //传入右值,不使用拷贝,转移str所有权
m_strArray.push_back("test"); //传入右值,不使用拷贝
4. 参考
- [1] 深入应用C++11
- [2] 一文读懂C++右值引用和std::move
以上便是本篇文章的所有内容了,如有错误或补充的地方,请大家帮忙指正,谢谢~
本文围绕C++11展开,介绍了左值与右值的区分方法,左值可被取地址且位于等号左边,右值反之。还阐述了左值引用、const左值引用、右值引用等概念,以及万能引用、引用折叠等规则。此外,讲解了std::move和std::forward的作用,并说明了如何利用移动构造函数和STL进行性能优化。
1999

被折叠的 条评论
为什么被折叠?



