首页 > 编程语言 >深入理解C++右值引用和移动语义:全面解析

深入理解C++右值引用和移动语义:全面解析

时间:2023-05-06 17:45:07浏览次数:41  
标签:右值 对象 左值 move 语义 C++ 引用 移动

C++11引入了右值引用,它也是C++11最重要的新特性之一。原因在于它解决了C++的一大历史遗留问题,即消除了很多场景下的不必要的额外开销。即使你的代码中并不直接使用右值引用,也可以通过标准库,间接地从这一特性中收益。为了更好地理解该特性带来的优化,以及帮助我们实现更高效的程序,我们有必要了解一下有关右值引用的意义。

什么是右值引用

右值

在引入右值的概念前,我们不妨先看看左值。一句话加以概括:左值就是等号左边的值;同理,右值也就是等号右边的值。举个例子:int a = 2;

这里的a是等号左边,可以通过取址符&来获取地址,所以是一个左值。而5在等号右边,无法通过取址符&来获取地址,所以只一个右值。

右值引用

左值引用是对于左值的引用或者叫别名。同样地,右值引用也就是对于右值引用。语法也很简单,就是在左值引用的语法之上在多加一个&,写成类型 &&右值引用名 = 右值;的形式即可,比如:

int &&a = 5;
a = 6;
string s1 = "hello";
string &&s2 = s1 + s1;
s2 += s1;

上述简单例子,展示了右值引用的基本用法。不过通常情况下,右值引用更多的是被用于处理函数参数。比如:

struct Student {
    Student(Student &&s);
};

为什么要使用右值引用

C++11之前,很多C++程序里存在大量的临时对象,又称无名对象。主要出现在如下场景:

  • 函数的返回值
  • 用户自定义类型经过一些计算后产生的临时对象
  • 值传递的形参

先说函数的返回值,最常见的类型就是某些返回用户自定义类型的时候,如果没有将其复制,就会产生临时对象,比如:

Student func1();    
// 返回一个Student对象...func1();            
// 调用了func1创建了一个Student对象,但是没有使用,于是编译器创建了一个临时对象来进行存储

然后是某些计算操作后产生的临时对象,比如:

Complex result = c1 + c2 + c3;  
// 编译器先计算c1 + c2的结果,并产生一个临时对象temp来存储结果,然后计算temp + c3的结果,然后将结果复制给result

还有值传递的方式的形参,例如:

void func(Student s);  
// 值传递...Student stu;func(stu);  
// 这里相当于是做了一次复制操作   Student s(stu);

而且这些临时对象随着生命周期的结束,编译器还会调用一次析构函数。随着这些操作次数的增加,或者当临时变量是个很大的类型时,这无疑会极大提高程序的开销,从而降低程序的效率。

C++11之后,随着右值引用的出现,可以有效的解决这些问题。通过move移动构造移动赋值运算符函数来获得临时对象的所有权,从而避免拷贝带来的额外开销,提高程序效率

移动构造

我们都知道,由于C++11之前,如果没有手动声明,编译器会给一个用于自定义类型(包括classstruct)自动生成的4个函数,分别是构造函数,拷贝构造函数,赋值运算符重载函数和析构函数。虽然通过传引用的方式,可以避免对象的复制。但是还是没法避免上述的临时对象的复制。而移动语义成功的解决的这个问题。

C++11之后,编译器自动生成的函数中又新增了2个,它们就是移动构造移动赋值运算符重载函数,通过它们,我们可以很好地实现对用户自定义类型的移动操作。而移动的本质就是获取临时对象的所有权,而不是通过复制的方式来获得。直接看代码:

class Foo {
   public:
    Foo(Foo &&rhs) : ptr_(rhs.ptr_) { rhs.ptr_ = nullptr; }
    Foo &operator=(Foo &&rhs) {
        if (*this != rhs) {
            ptr_ = rhs.ptr_;
            rhs.ptr_ = nullptr;
        }
        return *this;
    }

   private:
    int *ptr_;
};

Foo类重载了移动构造函数和移动赋值运算重载函数,使得Foo获得了移动的能力,当我们在面对产生临时的对象的时候,编译器就会根据传入的参数是左值还是右值来选择调用拷贝还是移动。如果是右值,就调用移动构造或移动赋值运算符函数。当Foo是一个很大的对象时候,就会极大的降低开销,提高程序效率。

move的应用场景

通过上述例子,我们可以看到移动并不是说完全没有开销,甚至有的时候开销并不一定比拷贝低,具体还是要看临时对象的大小和类型决定,例如:

vector<vector<int>> func() {
    vector<vector<int>> result;
    for (...) {
        vector<int> temp;
        ... temp.emplace_back(move(5));  // 没必要,直接传就行了        ...        result.emplace_back(move(temp)); //
                                         // ok,移动代替拷贝操作,提高了效率    }    return result;}

STL的大部分组件都支持移动语义,比如vectorstring等即可以通过move转换右值后调用移动构造函数进行移动操作来避免深拷贝。还有一些类是只允许移动,不允许拷贝,从而更让设计更符合逻辑,比如unique_ptr

move的原理

move函数的源码并不复杂:

template <class _Ty>
inline _CONST_FUN typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {
    return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}

我们可以一眼看到,move的实现其实就做了一件事,如果是左值,就通过static_cast将传进来的参数强转为右值并返回;如果是右值,甚至不用转换,直接返回。

右值移动的注意事项

  • 和左值移动一样,都需要直接初始化
  • 右值引用无法指向左值,除非使用move将其转成右值,否则编译报错
  • 当对象是基本类型的时候,没必要调用move,因为拷贝的开销可能还不如函数调用的开销大,尤其是在循环内的时候,需要仔细考虑
  • move并不会一定真的能移动,它只是将左值强转成右值,只有当该用户自定义类型重载了移动构造和移动运算符重载函数时才会进行移动操作
  • 现代编译在处理返回值的时候,通常都会进行返回值优化,尤其是标准库的组件,使用move来接收返回值反而会增加开销
  • 移动之后的对象就被析构,所以通常是对一些临时对象,或者不再使用的对象进行移动操作。如果还要继续使用该对象,就要使用拷贝而不是移动操作
  • 右值引用变量本身是个左值,如果想要右值引用指向右值引用,需要使用move转成右值
  • const 左值引用也可以指向右值,但是无法进行修改

标签:右值,对象,左值,move,语义,C++,引用,移动
From: https://www.cnblogs.com/xiaowange/p/17378110.html

相关文章

  • 《c++徒步》IO篇
    iostreamcincout参考链接:https://www.runoob.com/cplusplus/cpp-basic-input-output.html标准输出#include<iostream>usingnamespacestd;intmain(){charstr[]="HelloC++";cout<<"Valueofstris:"<<str&l......
  • C++一些bug的记录
    目录表达式必须具有类类型但它具有xxx类型表达式必须具有类类型但它具有xxx类型错误一般发生在使用.进行访问时,原因可能在于:你以为你定义了一个类对象,其实你是声明了一个函数,在编译器看来;对类对象指针采用.的方式访问其成员变量;也包括基本类型变量,错误地使用.inta......
  • 【已解决】Microsoft Visual C++ Redistributable is not installed
    【Error】导入torch,提示报错:MicrosoftVisualC++Redistributableisnotinstalled,thismayleadtotheDLLloadfailure.【Cause】Anaconda没有默认安装在C盘;系统没有安装VC++Redistributable程序。【Resolve】VC++Redistributable.exe双击安装,重启电脑即可。......
  • 《c++徒步》宏篇
    预处理命令参考链接:https://blog.csdn.net/akpe80900/article/details/102070084预处理命令是什么预处理语句,预处理语句是以#为起始标记,后面跟上预处理关键词。预处理功能,例如,宏定义、文件包括、条件编译等define语法://用来定义宏#define使用://定义常量#defineMAX_WI......
  • 高速爬过C++(3级)
    3级了!!不管做什么,我们都是在想适应所处的环境,真实的或者虚拟的。所以一个好的工具就该让我们在适应方面如虎添翼。因此C++程序就是个能接收环境的输入,然后经过处理,再输出到环境而环境里有什么,不外乎是万物,万物如何用C++来表达,或者C++里用什么来表示万物c++里用类型和变量变......
  • [Luogu-P1007]题解(C++)
    PartIPreface原题目(Luogu)PartIISketch给定一个正整数\(L\),表示独木桥长度。给定一个正整数\(N\),表示桥上士兵的数量。给定\(N\)个整数,分别表示每个士兵的坐标。规定走到\(0\)坐标或\(L+1\)的位置为下桥,两个士兵相遇时不能走过去,他们会各自回头走。求出所有士......
  • 高速爬过C++(2级)
    恭喜你升到2级,打怪不容易,虽然别的地方打一个怪可以升到99级!当我们用铅笔在白纸上画画时,会发生什么?我们弄脏了白纸。为啥会弄脏,原来是摩擦将铅笔粉留在了白纸上。虽然我们可以只关心画出什么样的图案而不管那些什么复杂的物理现象但是有时力道、速度也会影响成画的效果。所以......
  • 高速爬过C++(0级)
    魔镜:你想要什么?我:什么都想要魔镜:你有什么?我:什么都没有魔镜:我有的,你已经拥有。 掠过一本C++教程目录,似懂非懂的概念名称撞击大脑,让我两眼冒金星。魔镜什么都没有,所以我也什么都没有,心里默念了好几遍。我从0级开始闯入这本C++教程,虽然很久很久以前玩过,多少级?不说不说,好汉不......
  • [AtCoder-AT_ABC108_B]题解(C++)
    PartIPreface原题目(Luogu)原题目(AtCoder)PartIISketchPartIIIAnalysis观察这道题,我们很容易想到,必须推导出\(x1,y1,x2,y2\)与\(x3,y3,x4,y4\)之间的关系。我们观察下图。可以发现:\(\begin{aligned}\begin{cases}x3=x2-(y2-y1)\\y3=y2+(x2-......
  • [CodeForces-143A]题解(C++)
    PartIPreface原题目(Luogu)原题目(CodeForces)PartIISketch设有一个\(2\times2\)的棋盘,上面可以填入\(1-9\)的数字。给出\(6\)个数字,为每行每列以及每个对角线上的数字之和,求相应的摆放方式,无解输出\(-1\)。PartIIIAnalysis观察此题数据规模,不难发现数据......