C++右值与右值引用

引言

C++11中最重要的特性之一就是移动,这在大多数情况下可以大幅度的提升程序的性能,其实也不难理解,举个简单的例子,vector中当容量到达提前分配的最大值时会进行重新分配内存,那对于所有原本vector中所有的元素来说,要重新拷贝一份到新的分配的内存空间,那如果vector中提前数据较大,拷贝旧的数据,那是多大的内存开销啊。在C++11之前确实需要这样,但在移动这个新特性出现以后,一切都不一样了,在上面这个例子里,我们根本不需要拷贝再析构一份数据,只需要将控制权从原来的内存中转移到新的内存中即可,这便大幅度的提升了程序的性能。但要彻底清楚移动,我们首先来看看右值与右值引用。

左值与右值

我们平时遇到的表达式要么是左值,要么是右值,右值在C++98时就指纯右值,即就是临时变量,比如值传递的时拷贝的参数或者函数返回值是一个值时的临时拷贝对象,或者单纯的字面值 比如 int a = 5 中的5就是一个右值,而在C++11中为了支持移动,又引入了亡值,通常在移动后,移后源对象就会被调用析构函数,因为其中的值已经被转移,亡值这个名称也可以说是很通俗易懂了 比如std::move函数的返回值就是亡值 我们先来看一段代码来理解什么是亡值

int main()
{
    string a="hello";
    vector<int> aa={1,2,3};
    cout << &aa[2] << endl;
    vector<vector<int>>vec;
    vec.push_back(std::move(aa));
    cout << vec[0][2] << endl;
    cout << &vec[0][2] << endl;
    cout << aa.size() << endl;
}

输出结果:

0x55750d91be78
3
0x55750d91be78
0

在这里aa在std::move以后就成了亡值 其对象中的数据被vec“盗”走 在调用结束后aa被析构

所以我们可以对左值和右值做一个区分:

在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(亡值或纯右值)。:

右值引用

C++11中,为了支持移动操作,便引入了一种新的引用类型 右值引用 右值引用可以让我们避免不必要的拷贝,从而大幅度提升程序的性能,可以用来实现高效的库。
右值引用在语法上与普通的引用类似 左值引用就是类型后加一个&,右值引用就是加两个&,值得一提的是函数参数为左值引用,其实参可以是左值和右值,而函数参数是右值,实参只能是右值,这就引出了一个函数参数匹配的问题,左值引用既然兼容左值与右值,我们怎么让编译器知道何时为右值从而做出正确的处理呢,

        StrVec(const StrVec&);
        StrVec(StrVec&&) noexcept;

我们通常给左值引用加上const 这时对于右值来说显然参数为 && 的函数才是最优匹配,而对左值来说参数为const &的函数是最优匹配 这就解决了这个问题。

没有移动构造函数 右值也会被拷贝
引入右值引用就是为了参数为右值时把原来的拷贝替换为移动,从而提升程序性能,但如果我们仅是把参数换为右值,而没有相应的移动构造函数,我们的右值会向下例一样,所以我们在程序程序编写时,不但要注意参数问题,在进行实际操作的时候也要针对不同的参数做出不同的应对

void foo(X&& x)
{
  X anotherX = x;
}

那对于这个函数来说真的让程序的性能有所提示吗,答案当然是否定的,我们对于右值的判断先前是是否有名字,但这里好像是个例外,它既有名字,同时也是个右值,我们虽然清楚,但编译器并不知道,所以需要我们手动的使它变成一个右值,而我们前面提到了一个函数,就是std::move,它可以使一个对象变成一个亡值,这时在参数匹配时就会匹配 foo(&&) 而不是之前的foo(const &)了,这样也就达到了我们的目的,减少一次拷贝,提升程序性能。这是一个值得注意的点 不然我们只会以为性能提升,但实则任进行了拷贝。

void foo(X&& x)
{
  X anotherX = std::move(x);
}

这样才是正确的处理方式。

完美转发

这个问题是在大佬博客中发现的问题,顿时觉得C++博大精深,遂放在此篇博客中进行讨论,原问题链接在文末,侵删

什么是完美转发,就是通过引用折叠从而在接受一个参数时能够适用于所有的情况,包括右值与左值。

引用折叠
&& &  -》 &
&  && -》 &
&  &  -》 &
&& && —》 &&

template<typename fun,typename a,typename b>
void exec(fun f,a tmpa,b tmpb){
	f(a,b);
}

这样我们可以满足所有的情况 但是每次都有一次不必要的拷贝,你也许会说使用引用,没错,我们来看看

template<typename fun,typename a,typename b>
void exec(fun f,a &tmpa,b &tmpb){
	f(tmpa,tmpb);
}

int tmp(int a,int b){
    cout << a*b << endl;
}

int main()
{
    int a=5;
    int b=6;
    exec(tmp,a,b);
}

没有问题 编译正常,但是这就引来了一个问题,第一个版本与第二个版本看似相同,实则天差地别,因为第二个版本不能以右值作为参数 我们这样就会报错

exec(tmp,a,6);

报错原因是这样的
cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
我们不能够把一个右值绑定到一个左值引用上

你也许会想到上面提到的引用叠加 我们可以这样解决这个问题

template<typename fun,typename a,typename b>
void exec(fun f,a &&tmpa,b &&tmpb){
	f(tmpa,tmpb);
}

int tmp(int a,int b){
    cout << a*b << endl;
}

int main()
{
    int a=5;
    int b=6;
    exec(tmp,a,6);
}

在提一个问题 就是当我们要使用这个函数时呢

int tmp(int &&a,int &&b){
    cout << a*b << endl;
}

我们会发现一般的调用都会失败 会报这样的错
cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’

原因在于引用合并后T1和T2的的类型便变成了int,int 与 int**当然是无法被绑定的。

所以我们需要一个函数 std::forword() 这个c++11中的函数可以回复模板参数类型变化的问题

所以这样修改下就可以了

template<typename fun,typename a,typename b>
void exec(fun f,a &&tmpa,b &&tmpb){
	f(std::forward<a>(tmpa),std::forward<a>(tmpb));
}

int tmp(int &&a,int &&b){
    cout << a*b << endl;
}

int main()
{
    int a=5;
    int b=6;
    exec(tmp,std::move(a),std::move(b));
}[
](https://blog.csdn.net/craftsman1970/article/details/82191808)

std::move函数的使用时机需要斟酌

因为move函数的作用是把一个左值转化成一个亡值 这意味它的值已经被“盗” ,而我们不能除了赋值和销毁外对其做任何假设,所以意味着正确的使用会使得程序性能大幅度提升,而错误的使用会使的程序出现莫名其妙的错误。所以只有我们确定移动操作是安全的 时候才可以使用。

以上就是我在学习过之后的感触 希望对有同样问题的你有所帮助。

参考资料:
C++ primer plus
C++ primer
右值引用浅析
std::forward()

posted @ 2022-07-02 13:18  李兆龙的博客  阅读(270)  评论(0编辑  收藏  举报