C++11 右值引用及性能优化

本文围绕C++11展开,介绍了左值与右值的区分方法,左值可被取地址且位于等号左边,右值反之。还阐述了左值引用、const左值引用、右值引用等概念,以及万能引用、引用折叠等规则。此外,讲解了std::move和std::forward的作用,并说明了如何利用移动构造函数和STL进行性能优化。


使用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&&最为参数被左值引用或右值引用初始化时,就会发生引用折叠,关于折叠引用的规则如下:
    1. 所有右值引用叠加到右值引用上仍然是一个右值引用
    2. 所有的其他引用叠加到右值引用上将变为左值引用
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后会转移资源控制所有权,当前他必须应用在实现了移动构造函数的对象上,像intchar*基础类型仍然会发生拷贝

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);              //调用移动赋值函数

当然,赋值后datadata2转移了所有权,已经是空的数据了

以上两种方法的效率对比:

 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. 参考

以上便是本篇文章的所有内容了,如有错误或补充的地方,请大家帮忙指正,谢谢~

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值