前言

  • 在探讨c++11中的Move函数前,先介绍两个概念(左值和右值)

左值和右值

首先区分左值和右值

左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)

右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)

便携方法:对表达式取地址,如果能,则为左值,否则为右值

int val;
val = 4; // 正确 ①
4 = val; // 错误 ②

上述例子中,由于在之前已经对变量val进行了定义,故在栈上会给val分配内存地址,运算符=要求等号左边是可修改的左值,4是临时参与运算的值,一般在寄存器上暂存,运算结束后在寄存器上移除该值,故①是对的,②是错的

左值引用

右值引用

std::move函数

  • std::move作用主要可以将一个左值转换成右值引用,从而可以调用C++11右值引用的拷贝构造函数
  • std::move应该是针对你的对象中有在堆上分配内存这种情况而设置的,如下

remove_reference源码剖析

在分析std::move()std::forward()之前,先看看remove_reference,下面是remove_reference的实现:

template<typename _Tp>
struct remove_reference
{ typedef _Tp type; }; // 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; }; template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };

remove_reference的作用是去除T中的引用部分,只获取其中的类型部分。无论T是左值还是右值,最后只获取它的类型部分。

std::forward源码剖析

  • 转发左值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

先通过获得类型type,定义_t为左值引用的左值变量,通过static_cast进行强制转换。_Tp&&会发生引用折叠,当_Tp推导为左值引用,则折叠为_Tp& &&,即_Tp&,当推导为右值引用,则为本身_Tp&&,即forward返回值与static_cast处都为_Tp&&

  • 转发右值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}

不同于转发左值,_t为右值引用的左值变量,除此之外中间加了一个断言,表示当不是左值的时候,也就是右值,才进行static_cast转换。

std::move()源码剖析

// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

std::move的功能是:

  • 传递的是左值,推导为左值引用,仍旧static_cast转换为右值引用。

  • 传递的是右值,推导为右值引用,仍旧static_cast转换为右值引用。

  • 在返回处,直接范围右值引用类型即可。还是通过renive_reference获得_Tp类型,然后直接type&&即可。

所以std::remove_reference<_Tp>::type&&,就是一个右值引用,我们就知道了std::move干的事情了。

小结

  • 在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。
  • std::move()与std::forward()都仅仅做了类型转换(可理解为static_cast转换)而已。真正的移动操作是在移动构造函数或者移动赋值操作符中发生的
  • 在类型声明当中, “&&” 要不就是一个 rvalue reference ,要不就是一个 universal reference – 一种可以解析为lvalue reference或者rvalue reference的引用。对于某个被推导的类型T,universal references 总是以 T&& 的形式出现。
  • 引用折叠是 会让 universal references (其实就是一个处于引用折叠背景下的rvalue references ) 有时解析为 lvalue references 有时解析为 rvalue references 的根本机制。引用折叠只会在一些特定的可能会产生"引用的引用"场景下生效。这些场景包括模板类型推导,auto 类型推导, typedef 的形成和使用,以及decltype 表达式。

std::move使用场景

  • 在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。 在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数拷贝构造函数赋值运算符重载析构函数等。深拷贝/浅拷贝在此不做讲解。
class Array {
public:
Array(int size) : size_(size) {
data = new int[size_];
} // 深拷贝构造
Array(const Array& temp_array) {
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
} // 深拷贝赋值
Array& operator=(const Array& temp_array) {
delete[] data_; size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
} ~Array() {
delete[] data_;
} public:
int *data_;
int size_;
};

该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,如:

class Array {
public:
Array(int size) : size_(size) {
data = new int[size_];
} // 深拷贝构造
Array(const Array& temp_array) {
...
} // 深拷贝赋值
Array& operator=(const Array& temp_array) {
...
} // 移动构造函数,可以浅拷贝
Array(const Array& temp_array, bool move) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
} ~Array() {
delete [] data_;
} public:
int *data_;
int size_;
};

这么做有2个问题:

  • 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
  • 无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){...},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);这种调用方式就没法用了。

可以发现左值引用真是用的很不爽,右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以右值引用为参数移动构造函数移动赋值重载函数,或者其他函数,最常见的如std::vector的push_backemplace_back。参数为左值引用意味着拷贝,为右值引用意味着移动。

class Array {
public:
...... // 优雅
Array(Array&& temp_array) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
} public:
int *data_;
int size_;
};

如何使用:

// 例1:Array用法
int main(){
Array a; // 做一些操作
..... // 左值a,用std::move转化为右值
Array b(std::move(a));
}

实例:vector::push_back使用std::move提高性能

// 例2:std::vector和std::string的实际例子
int main() {
std::string str1 = "aacasxs";
std::vector<std::string> vec; vec.push_back(str1); // 传统方法,copy
vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
vec.emplace_back("axcsddcas"); // 当然可以直接接右值
} // std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val); void emplace_back (Args&&... args);

在vector和string这个场景,加个std::move会调用到移动语义函数,避免了深拷贝。

除非设计不允许移动,STL类大都支持移动语义函数,即可移动的。 另外,编译器会默认在用户自定义的classstruct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造等函数(具体规则自行百度哈)。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝)

std::unique_ptr<A> ptr_a = std::make_unique<A>();

std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型

std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过

std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。

std::vector<int> b(5);
b[0] = 2;
b[1] = 2;
b[2] = 2;
b[3] = 2; // 此处用move就不会对b中已有元素重新进行拷贝构造然后再放到a中
std::vector<int> a = std::move(b);

将vector B赋值给另一个vector A,如果是拷贝赋值,那么显然要对B中的每一个元素执行一个copy操作到A,如果是移动赋值的话,只需要将指向B的指针拷贝到A中即可,试想一下如果vector中有相当多的元素,那是不是用move来代替copy就显得十分高效了呢?建议看一看Scott Meyers 的Effective Modern C++,里面对移动语义、右值引用以及类型推导进行了深入的探索

万能引用

首先,我们先看一个例子

#include <iostream>

using std::cout;
using std::endl; template<typename T>
void func(T& param) {
cout << param << endl;
} int main() {
int num = 2019;
func(num);
return 0;
}

这样例子的编译输出不存在什么问题,但是如果修改成下面的调用方式呢?

int main(){
func(2019);
return 0;
}

编译器会产生错误,因为上面的模板函数只能接受左值或者左值引用(左值一般是有名字的变量,可以取到地址的),我们当然可以重载一个接受右值的模板函数,如下也可以达到效果

template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
} int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}

输出结果

传入的是左值
传入的是右值

第一次函数调用的是左值得版本,第二次函数调用的是右值版本。但是,有没有办法只写一个模板函数即可以接收左值又可以接收右值呢?

C++11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值

但是注意了:只有发生类型推导的时候,T&&才表示万能引用(如模板函数传参就会经过类型推导的过程);否则,表示右值引用

所以,上面的案例我们可以修改为

template<typename T>
void func(T&& param) {
cout << param << endl;
} int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}

引用折叠

万能引用说完了,接着来聊引用折叠(Reference Collapse),因为完美转发(Perfect Forwarding)的概念涉及引用折叠。一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:

  • 左值-左值 T& & # 函数定义的形参类型是左值引用,传入的实参是左值引用
template<typename T>
void func(T& param) {
cout << param << endl;
} int main(){
int num = 2021;
int& val = num;
func(val);
}
  • 左值-右值 T& && # 函数定义的形参类型是左值引用,传入的实参是右值引用
template<typename T>
void func(T& param) {
cout << param << endl;
} int main(){
int&& val = 2021;
func(val);
}
  • 右值-左值 T&& & # 函数定义的形参类型是右值引用,传入的实参是左值引用
template<typename T>
void func(T&& param) {
cout << param << endl;
} int main(){
int num = 2021;
int& val = num;
func(val);
}
  • 右值-右值 T&& && # 函数定义的形参类型是右值引用,传入的实参是右值引用
template<typename T>
void func(T&& param) {
cout << param << endl;
} int main(){
int&& val = 4;
func(val);
}

但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果才是右值引用

即就是前面三种情况代表的都是左值引用,而第四种代表的右值引用

完美转发

下面接着说完美转发(Perfect Forwarding),首先,看一个例子

#include <iostream>

using std::cout;
using std::endl; template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
} template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
} template<typename T>
void warp(T&& param) {
func(param);
} int main() {
int num = 2019;
warp(num);
warp(2019);
return 0;
}

输出的结果

传入的是左值
传入的是左值

是不是和预期的不一样,下面我们来分析一下原因:

warp()函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个warp()函数调用实参是左值,所以,warp()函数中调用func()中传入的参数也应该是左值;第二个warp()函数调用实参是右值,根据上面所说的引用折叠规则,warp()函数接收的参数类型是右值引用,那么为什么却调用了调用func()的左值版本了呢?这是因为在warp()函数内部,右值引用类型变为了左值,因为参数有了名称,我们也通过变量名取得变量地址

那么问题来了,怎么保持函数调用过程中,变量类型的不变呢?这就是我们所谓的“变量转发”技术,在C++11中通过std::forward()函数来实现。我们来修改我们的warp()函数如下:

template<typename T>
void warp(T&& param) {
func(std::forward<T>(param));
}

则可以输出预期的结果

传入的是左值
传入的是右值

参考博文

现代C++之万能引用、完美转发、引用折叠(万字长文):https://blog.csdn.net/guangcheng0312q/article/details/103572987

C++ 中的「移动」在内存或者寄存器中的操作是什么,为什么就比拷贝赋值性能高呢?:https://www.zhihu.com/question/55735384

一文读懂C++右值引用和std::move:https://zhuanlan.zhihu.com/p/335994370

一文带你详细介绍c++中的std::move函数的更多相关文章

  1. [转]详细介绍java中的数据结构

    详细介绍java中的数据结构 本文介绍的是java中的数据结构,本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类.一起来看本文吧! 也许你已经熟练使用了java.util包里面的各 ...

  2. 详细介绍java中的数据结构

    详细介绍java中的数据结构 http://developer.51cto.com/art/201107/273003.htm 本文介绍的是java中的数据结构,本文试图通过简单的描述,向读者阐述各个 ...

  3. 详细介绍Java中的堆、栈和常量池

    下面主要介绍JAVA中的堆.栈和常量池: 1.寄存器 最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 栈 存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在 ...

  4. 关于C++11中的std::move和std::forward

    std::move是一个用于提示优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁,其中又尤其是返回字符串std::st ...

  5. [C/C++]关于C++11中的std::move和std::forward

    http://www.cnblogs.com/cbscan/archive/2012/01/10/2318482.html http://blog.csdn.net/fcryuuhou/article ...

  6. 详细介绍php中的命名空间

    php命名空间的一个最明确的作用是解决重名问题,PHP中不允许两个函数或者类出现相同的名字,否则会产生一个致命的错误.上一章节介绍了什么是php命名空间.php官网已很明确的进行了定义并形象化解释,这 ...

  7. 详细介绍Java中的堆和栈

    栈与堆都是Java用来在RAM中寄存数据的中央.与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆. Java的堆是一个运转时数据区,类的对象从中分配空间.这些对象经过new.newar ...

  8. pytest文档22-fixture详细介绍-作为参数传入,error和failed区别

    前言 fixture是pytest的核心功能,也是亮点功能,熟练掌握fixture的使用方法,pytest用起来才会得心应手! fixture简介 fixture的目的是提供一个固定基线,在该基线上测 ...

  9. 详细介绍javascript中的几种for循环的区别

    偶然间见到了forEach循环,感觉很新奇,就研究了一下,顺带着把js中的几种for循环做了一个比较. 首先,简单说一下,js中一共大概有四种for循环:(1).那种简单常见的for循环:(2).fo ...

随机推荐

  1. Django之cookie 与session组件

    一.会话跟踪技术 1.1 什么是会话跟踪 我们需要先了解一下什么是会话!可以把会话理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应.例如你给10086打个电话,你就是客户端,而 ...

  2. JWT加密解密方法

    public static string Key { get; set; } = "123456789987654321";//解密串 /// <summary> // ...

  3. SpringBoot学习笔记(四)

    本文主要介绍:SpringBoot开发中如何自定义starter 1.什么是starter Starter可以理解为一个可拔插式的插件,提供一系列便利的依赖描述符,您可以获得所需的所有Spring和相 ...

  4. 当红开发语言Go,真的是未来的技术主流吗?

    摘要:文将详细介绍 Golang 的语言特点以及它的优缺点和适用场景,带着上述几个疑问,为读者分析 Go 语言的各个方面,以帮助初入 IT 行业的程序员以及对 Go 感兴趣的开发者进一步了解这个热门语 ...

  5. Spring笔记(五)

    Spring 事务操作 一.事务(概念) 1. 什么是事务 事务是数据库的最基本单元,逻辑上的一组操作,要么都成功,如果有一个失败,那么所有的操作都失败 典型场景: lucy转账100元给mary l ...

  6. Java学习之this关键字的使用

    •区分成员变量和局部变量 public class Person { String name; int age; public void set(String name,int age) { this ...

  7. 2021精选 Java面试题附答案(一)

    1.什么是Java Java是一门面向对象的高级编程语言,不仅吸收了C++语言的各种优点,比如继承了C++语言面向对象的技术核心.还摒弃了C++里难以理解的多继承.指针等概念,,同时也增加了垃圾回收机 ...

  8. 梳理一下最近准备蓝桥杯时学习DP问题的想法

    学习时间不长,记录的只是学习过程的思路和想法,不能保证正确,代码可以在acwing上AC. 01背包问题: 1.首先是简单的01背包问题 2.先确定状态,f[i][j]表示有第i件物品,时间为j的最大 ...

  9. 解决Spring中使用Example无法查询到Mongodb中的数据问题

    1 问题描述 在Spring Boot中使用Mongodb中的Example查询数据时查询不到,示例代码如下: ExampleMatcher matcher = ExampleMatcher.matc ...

  10. Prometheus【node_exporter】+grafana监控云主机

    下面说一下这个开源软件的安装实践过程,目标如下: 在监控服务器上安装prometheus 在被监控环境上安装exporter 安装grafana 在监控服务器上安装prometheus 开始安装pro ...