C++中的右值引用与移动构造函数
1.右值引用
右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector、std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能。
1.1左值、右值的纯右值、将亡值、右值
要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。
左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。例如:
#include <type_traits>
int main() {// 正确,"01234" 类型为 const char [6],因此是左值const char (&left)[6] = "01234";// 断言正确,确实是 const char [6] 类型,注意 decltype(expr) 在 expr 是左值// 且非无括号包裹的 id 表达式与类成员表达式时,会返回左值引用static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, "");// 错误,"01234" 是左值,不可被右值引用// const char (&&right)[6] = "01234";
}但是注意,数组可以被隐式转换成相对应的指针类型,而转换表达式的结果(如果不是左值引用)则一定是个右值(右值引用为将亡值,否则为纯右值)。例如:
const char* p = "01234"; // 正确,"01234" 被隐式转换为 const char*
const char*&& pr = "01234"; // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
// const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值
将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
将亡值可能稍有些难以理解,我们来看这样的代码:
std::vector<int> foo() {std::vector<int> temp = {1, 2, 3, 4};return temp;
}
std::vector<int> v = foo();在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast<std::vector<int> &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。
1.2右值引用和左值引用
要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,例如:
#include <iostream>
#include <string>
void reference(std::string& str) {std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {std::cout << "右值" << std::endl;
}
int main()
{std::string lv1 = "string,"; // lv1 是一个左值// std::string&& r1 = lv1; // 非法, 右值引用不能引用左值std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值std::cout << rv1 << std::endl; // string,const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期// lv2 += "Test"; // 非法, 常量引用无法被修改std::cout << lv2 << std::endl; // string,string,std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期rv2 += "Test"; // 合法, 非常量引用能够修改临时变量std::cout << rv2 << std::endl; // string,string,string,Testreference(rv2); // 输出左值return 0;
}rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。
注意,这里有一个很有趣的历史遗留问题,我们先看下面的代码:
#include <iostream>
int main() {// int &a = std::move(1); // 不合法,非常量左引用无法引用右值const int &b = std::move(1); // 合法, 常量左引用允许引用右值std::cout << a << b << std::endl;
}
第一个问题,为什么不允许非常量引用绑定到非左值?这是因为这种做法存在逻辑错误:
void increase(int & v) {v++;
}
void foo() {double s = 1;increase(s);
}由于 int& 不能引用 double 类型的参数,因此必须产生一个临时值来保存 s 的值, 从而当 increase() 修改这个临时值时,调用完成后 s 本身并没有被修改。
第二个问题,为什么常量引用允许绑定到非左值?原因很简单,因为 Fortran 需要。
2.移动构造函数
传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作, 调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。 试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、 再把原来的东西全部扔掉(销毁),这是非常反人类的一件事情。
传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,例如:
#include <iostream>
class A {
public:int *pointer;A():pointer(new int(1)) {std::cout << "构造" << pointer << std::endl;}A(A& a):pointer(new int(*a.pointer)) {std::cout << "拷贝" << pointer << std::endl;} // 无意义的对象拷贝A(A&& a):pointer(a.pointer) {a.pointer = nullptr;std::cout << "移动" << pointer << std::endl;}~A(){std::cout << "析构" << pointer << std::endl;delete pointer;}
};
// 防止编译器优化
A return_rvalue(bool test) {A a,b;if(test) return a; // 等价于 static_cast<A&&>(a);else return b; // 等价于 static_cast<A&&>(b);
}
int main() {A obj = return_rvalue(false);std::cout << "obj:" << std::endl;std::cout << obj.pointer << std::endl;std::cout << *obj.pointer << std::endl;return 0;
}在上面的代码中:
首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。
从而避免了无意义的拷贝构造,加强了性能。再来看看涉及标准库的例子:
#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string
int main() {std::string str = "Hello world.";std::vector<std::string> v;// 将使用 push_back(const T&), 即产生拷贝行为v.push_back(str);// 将输出 "str: Hello world."std::cout << "str: " << str << std::endl;// 将使用 push_back(const T&&), 不会出现拷贝行为// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销// 这步操作后, str 中的值会变为空v.push_back(std::move(str));// 将输出 "str: "std::cout << "str: " << str << std::endl;return 0;
}2.1完美的移动转发
前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:
void reference(int& v) {std::cout << "左值" << std::endl;
}
void reference(int&& v) {std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {std::cout << "普通传参:";reference(v); // 始终调用 reference(int&)
}
int main() {std::cout << "传递右值:" << std::endl;pass(1); // 1是右值, 但输出是左值std::cout << "传递左值:" << std::endl;int l = 1;pass(l); // l 是左值, 输出左值return 0;
}对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。 因此 reference(v) 会调用 reference(int&),输出『左值』。 而对于pass(l)而言,l是一个左值,为什么会成功传递给 pass(T&&) 呢?
这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用, 但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用, 既能左引用,又能右引用。但是却遵循如下规则:
函数形参类型 | 实参参数类型 | 推导后函数形参类型 |
T& | 左引用 | T& |
T& | 右引用 | T& |
T&& | 左引用 | T& |
T&& | 右引用 | T&& |
因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。 更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递。
完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):
#include <iostream>
#include <utility>
void reference(int& v) {std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {std::cout << " 普通传参: ";reference(v);std::cout << " std::move 传参: ";reference(std::move(v));std::cout << " std::forward 传参: ";reference(std::forward<T>(v));std::cout << "static_cast<T&&> 传参: ";reference(static_cast<T&&>(v));
}
int main() {std::cout << "传递右值:" << std::endl;pass(1);std::cout << "传递左值:" << std::endl;int v = 1;pass(v);return 0;
}输出结果为:
传递右值:
普通传参: 左值引用std::move 传参: 右值引用std::forward 传参: 右值引用
static_cast<T&&> 传参: 右值引用
传递左值:普通传参: 左值引用std::move 传参: 右值引用std::forward 传参: 左值引用
static_cast<T&&> 传参: 左值引用无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发, 所以 std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。
唯独 std::forward 即没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。
std::forward 和 std::move 一样,没有做任何事情,std::move 单纯的将左值转化为右值, std::forward 也只是单纯的将参数做了一个类型的转换,从现象上来看, std::forward<T>(v) 和 static_cast<T&&>(v) 是完全一样的。
读者可能会好奇,为何一条语句能够针对两种类型的返回对应的值, 我们再简单看一看 std::forward 的具体实现机制,std::forward 包含两个重载:
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
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);
}在这份实现中,std::remove_reference 的功能是消除类型中的引用, std::is_lvalue_reference 则用于检查类型推导是否正确,在 std::forward 的第二个实现中 检查了接收到的值确实是一个左值,进而体现了坍缩规则。
当 std::forward 接受左值时,_Tp 被推导为左值,所以返回值为左值;而当其接受右值时, _Tp 被推导为 右值引用,则基于坍缩规则,返回值便成为了 && + && 的右值。 可见 std::forward 的原理在于巧妙的利用了模板类型推导中产生的差异。
这时我们能回答这样一个问题:为什么在使用循环语句的过程中,auto&& 是最安全的方式? 因为当 auto 被推导为不同的左右引用时,与 && 的坍缩组合是完美转发。
相关文章:
C++中的右值引用与移动构造函数
1.右值引用右值引用是 C11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C 中大量的历史遗留问题, 消除了诸如 std::vector、std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能。1.1左值、右值的纯右值、将亡值…...
Swift如何使用依赖注入进行解藕
Swift 中可以使用依赖注入(Dependency Injection)来解耦组件之间的依赖关系。依赖注入是一种设计模式,指的是在运行时,将一个组件所依赖的其他组件通过构造函数或者属性注入的方式传递给该组件。 例如,有两个组件 A 和…...
合宙ESP32S3-CORE开发板|保姆级|Arduino IDE|windows11|esp32S3支持库|helloword例程:Arduino 环境搭建
Arduino主页网址: Software | Arduino 以windows11版本为例: Arduino IDE最新版本为2.0.3 左边的按钮是直接下载(免捐赠): 下载安装完成后,更改软件默认语言: 默认的库是不支持ESP32的&#…...
CMake中target_precompile_headers的使用
CMake中的target_precompile_headers命令用于添加要预编译的头文件列表,其格式如下: target_precompile_headers(<target><INTERFACE|PUBLIC|PRIVATE> [header1...][<INTERFACE|PUBLIC|PRIVATE> [header2...] ...]) # 1 target_preco…...
SpringCloud和微服务介绍
SpringCloud介绍 SpringCloud是在SpringBoot的基础上构建的,用于简化分布式系统构建的工具集。 该工具集为微服务架构中所涉及的配置管理,服务发现,智能路由,断路器,微代理和控制总线等操作提供了一种简单的开发方式。 SpringCloud中包含了多个子项目: Spring …...
Qt源码编译过程中配置文件中的选项说明
文章目录选项说明默认值顶级安装目录-prefix 部署目录,如目标设备上所示。/usr/local/Qt-$QT_VERSION-extprefix 安装目录,如主机上所示。SYSROOT/PREFIX-hostprefix [dir]主机上运行的生成工具的安装目录。如果未给定[dir],则将使用当前构建…...
Mysql 增删改查(一) —— 查询(条件查询where、分页limits、排序order by、分组 group by)
查询 select 可以认为是四个基本操作中使用最为频繁的操作,然而数据量比较大的时候,我们不可能查询所有内容,我们一般会搭配其他语句进行查询: 假如要查询某一个字段的内容,可以使用 where假如要查询前几条记录&#…...
VScode 结合clangd 构建linux源代码阅读环境
1、背景介绍上一篇文章:VScode 结合Global构建linux源代码阅读环境 ,介绍了在VS Code工具中通过remote-ssh远程登陆到Linux远程服务器,使用Global构建linux源代码阅读环境,对linux kernel代码进行解析,实现全局搜索、自…...
web应用 —— JavaScript
Web应用(acwing) 三、JavaScript 1.JS的调用方式与执行顺序 ①使用方法 HTML页面中任意位置加上<script type"module"></script>标签即可 常见使用方式: 直接在<script type"module"></script>标签内写JS代码。直…...
SSM整合SpringSecurity简单使用
一、SpringSecurity 1.1 什么是SpringSecurity Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。(官网地址) Spring Security 为基于J2EE企业应用软件提供了全面安全服务。特别是使用领先的J2EE解决方案-Spring框架开发…...
Java零基础教程——数据类型
目录数据类型数据类型的分类运算符算术运算符符号做连接符的识别自增、自减运算符赋值运算符关系运算符逻辑运算符短路逻辑运算符三元运算符运算符优先级数据类型 数据类型的分类 引用数据类型(除基本数据类型之外的,如String ) 基本数据类…...
【Linux 信号】信号的产生方式、信号的捕捉的全过程
信号的处理方式是远远比信号的产生当闹钟响了就知道时间到了,说明对应信号处理方法比信号产生更早操作系统的信号处理方法在编写操作系统的时候就已经编写好了signal函数1.1所有的信号1.2 signal函数的概念和简单使用捕捉信号就是自定义对应的信号的处理方法9号信号…...
代码随想录第58天(动态规划):● 392.判断子序列 ● 115.不同的子序列
一、判断子序列 题目描述: 思路和想法: 这道题目还是最长公共子序列的拓展,只是这里进行删除的一定是t字符串,当不相等时,dp[i][j] dp[i][j - 1];其余基本一致。当最长公共子序列个数等s.size()时&#x…...
代码随想录第55天(动态规划):● 309.最佳买卖股票时机含冷冻期 ● 714.买卖股票的最佳时机含手续费
一、最佳买卖股票时机含冷冻期 题目描述: 思路和想法: 这道题相较于之前的题目,注重对状态的分析,这里分为四个状态。 (1)状态一,买入状态 dp[i][0] 操作一:前一天就是持有状态(状…...
字符串装换整数(atoi)-力扣8-java
一、题目描述请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C 中的 atoi 函数)。函数 myAtoi(string s) 的算法如下:读入字符串并丢弃无用的前导空格检查下一个字符(假设还未…...
毕业5年,从月薪3000到年薪40w,我掌握了那些核心技能?(建议收藏)
大家好,我是静静~~是一枚一线大厂的测试开发工程师很多读者私信问我,自己时间不短了,随着工作年限的不断增长,感觉自己的技术水平与自己的工作年限严重不符。想跳槽出去换个新环境吧,又感觉自己的能力达不到心仪公司的…...
C++中的并行与并发
1.1 并行基础std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 <thread> 头文件, 它提供了很多基本的线程操作,例如 get_id() 来获取所创建线程的线程 ID,使用 join() 来加入…...
h2database源码解析-如何更新一条行记录
这里的更新包括两种操作:删、改。更新操作涉及的内容在其他文章里面已经做过介绍了,本文主要是介绍更新的代码流程,以了解更新操作都做了哪些事情。如果有未介绍过的知识点会详细介绍。 目录改(update)如何判读是否加了行锁删(delete)改(upda…...
FyListen——生命周期监听器(设计原理之理解生命周期)
FyListen——生命周期监听器(设计原理之理解生命周期) FyListen 的核心原理有两个: 通过子Fragment对Activity、Fragment进行生命周期监听Java8 接口特性 default 1. 什么是上下文Context 这是一个装饰器模式, ContextImpl 是 …...
Element UI框架学习篇(六)
Element UI框架学习篇(六) 1 删除数据 1.1 前台核心函数 1.1.1 elementUI中的消息提示框语法 //①其中type类型和el-button中的type类型是一致的,有info灰色,success绿色,danger红色,warning黄色,primary蓝色 //②message是你所要填写的提示信息 //③建议都用,因为比双引号…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
