首页 > 编程语言 >C++右值引用与移动语义

C++右值引用与移动语义

时间:2023-02-12 17:45:08浏览次数:45  
标签:右值 左值 move 语义 C++ 引用 移动

一文看懂C++右值引用和移动语义

目录

  • 背景

  • 什么是右值引用

  • 为什么需要右值引用

  • 移动构造

  • move的原理

  • move的应用场景

  • 右值引用注意事项

  • 总结

背景

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_) {
        delete rhs.ptr_;
    }
    
    Foo &operator(Foo &&rhs) {
        if (*this != rhs) {
            ptr_ = rhs.ptr_;
            delete rhs.ptr_;
        }
        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/17106961.html

相关文章

  • C++中static关键字的作用
    static是什么在最开始C中引入了static关键字可以用于修饰变量和函数,后来由于C++引入了class的概念,现在static可以修饰的对象分为以下5种:成员变量,成员函数,普通函数,局部变......
  • C++ | 引用
    01.引用概述1.1创建引用变量引用是已定义的变量的别名(另一个名称)。inta;int&b=a;//将b作为a变量的别名C和C++使用&符号来指示变量的地址。C++给&符号赋予了另......
  • 【数据结构与算法】图论算法(C++实现)
    一些基本概念图一个图\(G=(V,E)\)由顶点集V和边集E组成。每一条边就是一副顶点对\((u,v)\),其中\(u,v\inV\)。顶点u和顶点v邻接当且仅当\((u,v)\inE\)......
  • C/C++工业数据分析与文件信息管理系统[2023-02-12]
    C/C++工业数据分析与文件信息管理系统[2023-02-12]程序设计基础A课程设计工业数据分析与文件信息管理系统题目背景智能制造是以工业生产数据分析、自动化技术为基础,具......
  • C++中的默认初始化
    当没有通过初始化器构造一个对象时,那么将会执行默认初始化。语法Tobject;newT;默认初始化出现在以下几种情况中当一个自动,静态,或者线程存储周期的变量声明......
  • C++中explicit的用法
    C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),承担了两个角色。1是个构造;2是个默认且隐含的类型转换操作符。所以,有时候在我们......
  • C++匿名函数
    #include<iostream>std::stringaaa="111";intmain(){//[]称为lambda捕获列表,它的含义为:告诉编译器需要在匿名函数内部使用外部的变量。捕获列表中的内容......
  • 学习C++第五天
    今日主要学习指针和结构体,并对前几天的知识进行一个测试总结指针和结构体#define_CRT_SECURE_NO_WARNINGS1#include<stdio.h>//intmain()//{// inta=10;//a在内存......
  • c++ 可以检测线程是否销毁的thread_guard
    #include<bits/stdc++.h>usingnamespacestd;constintN=1e6+1e2,Q=N;usingll=longlong;intn,q;lla[N];intsta[N];structq_t{ intl,r;}que[Q......
  • C++ 调用 Python 总结(一)
    PS:要转载请注明出处,本人版权所有。PS:这个只是基于《我自己》的理解,如果和你的原则及想法相冲突,请谅解,勿喷。前置说明  本文作为本人csdnblog的主站的备份。(BlogID......