C++ lambda 内 std::move 失效问题的思考
最近在学习 C++ Move 时,有看到这样一个代码需求:在 lambda 中,将一个捕获参数 move 给另外一个变量。
看似一个很简单常规的操作,然而这个 move 动作却没有生效。
具体代码如下
std::vector<int> vec = {1,2,3};
auto func = [=](){
auto vec2 = std::move(vec);
std::cout << vec.size() << std::endl; // 输出:3
std::cout << vec2.size() << std::endl; // 输出:3
};
代码可在 wandbox 运行。
我们期望的是,将对变量 vec
调用 std::move 后,数据将会移动至变量 vec2
, 此时 vec
里面应该没有数据了。但是通过打印 vec.size()
发现 vec 中的数据并没有按预期移走。
这也就意味着,构造 vec2 时并没有按预期调用移动构造函数,而是调用了拷贝构造函数。
为什么会造成这个问题呢, 我们需要结合 std::move
和 lambda
的原理看下。(最终的解决方案可以直接看 这里)
std::move 的本质
\(std::move()\) 位于
#include <utilty>
中,但一般无需特地引入,iostream
,string
等头文件会包含
对于 \(std::move\),有两点需要注意:
- \(std::move\) 中到底做了什么事情
- \(std::move\) 是否可以保证数据一定能移动成功
对于第二点来说,答案显然是不能。这也是本文的问题所在。那么 std::move 实际上是做了什么事情呢?
对于 std::move,其实现大致如下:
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
从代码可以看出,std::move 本质上是调用了 static_cast 做了一层强制转换,强制转换的目标类型是 remove_reference_t<T>&&
,remove_reference_t 是为了去除类型本身的引用,例如左值引用。总结来说,std::move 本质上是将对象强制转换为了右值引用。
那么,为什么我们通常使用 std::move 实现移动语义,可以将一个对象的数据移给另外一个对象?
这是因为 std::move 配合了移动构造函数使用,本质上是移动构造函数起了作用。移动构造函数的一般定义如下:
class A
{
public:
A (A &&);
}
可以看到移动构造函数的参数就是个右值引用 A&&
,因此 A a = std::move(b);
, 本质上是先将 b 强制转化了右值引用 A&&
,
然后触发了移动构造函数,在移动构造函数中,完成了对象 b 的数据到对象 a 的移动。
那么,在哪些情况下,A a = std::move(b);
会失效呢?
显然是,当 std::move 强转后的类型不是 A&&
,这样就不会命中移动构造函数。
例如:
const std::string str = "123";
std::string str2(std::move(str));
这个时候,对 str 对象调用 std::move
,强转出来的类型将会是 const string&&
, 这样移动构造函数就不会起作用了,但是这个类型却可以令复制构造函数生效。
结合本文最初的问题,在 lambda 中 move 没有生效,显然也是 std::move 强转的类型不是 std::vector<int>&&
, 才导致了没有 move 成功。
那么,为什么会出现这个问题呢,我们需要理解下 lambda 的工作原理。
lambda 闭包原理
对于 c++ 的 lambda,编译器会将 lambda 转化为一个独一无二的闭包类。而 lambda 对象最终会转化成这个闭包类的对象。
对于本文最初的这个 lambda 来说,最终实际上转化成了这么一个类型
// 转换前
auto func = [=](){
auto vec2 = std::move(vec);
};
// 转换后
class ClosureFunc{
public:
void operator() const{
auto vec2 = std::move(vec);
};
private:
std::vector<int> vec;
};
ClosureFunc func;
这里需要注意, lambda 的默认行为是,生成的闭包类的 operator()
默认被 const 修饰。
那么这里问题就来了,当调用 operator()
时, 该闭包类所有的成员变量也是被 const 修饰的,此时对成员变量调用 std::move
将会引发上文中提到的,强转出来的类型将会是 const string&&
问题。因此,移动构造函数将不会被匹配到。
我们最初的问题 lambda 中 std::move 失效的问题,也是因为这个原因。这也很符合 const 函数的语义: const 函数是不能修改成员变量的值。
解决方案
那么,这个应该怎么解决呢?答案是 mutable
。即在 lambda 尾部声明一个 mutable,如下:
auto func = [=]() mutable{
auto vec2 = std::move(vec);
};
这样编译器生成的闭包类的 operator()
将会不带 const 了。我们的 std::move 也可以正常转换,实现移动语义了。
std::vector<int> vec = {1,2,3};
auto func = [=]() mutable{
auto vec2 = std::move(vec);
std::cout <<vec.size() << std::endl; // 输出:0
std::cout <<vec2.size() << std::endl; // 输出:3
};
代码可以在 wandbox 运行。
参考
- Lambda 表达式 - cppreference
- Effective Modern c++
- 关于 C++ 右值及 std::move() 的疑问?
C++ lambda 内 std::move 失效问题的思考的更多相关文章
- 第19课 lambda vs std::bind
一. std::bind (一)std::bind实现的关键技术 [编程实验]探索bind原理,实现自己的bind函数 #include <iostream> #include <t ...
- 左值 lvalue,右值 rvalue 和 移动语义 std::move
参考文章: [1] 基础篇:lvalue,rvalue和move [2] 深入浅出 C++ 右值引用 [3] Modern CPP Tutorial [4] 右值引用与转移语义 刷 Leetcode ...
- lambda+mutable配合move实现单函数多程序域
主代码 //-----------------------------------说明一的代码 void fun0{ int t = 10; auto loopFun = [=]() mutable{ ...
- 关于C++11中的std::move和std::forward
std::move是一个用于提示优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁,其中又尤其是返回字符串std::st ...
- 右值引用和std::move函数(c++11)
1.对象移动 1)C++11新标准中的一个最主要的特性就是移动而非拷贝对象的能力 2)优势: 在某些情况下,从旧内存拷贝到新内存是不必要的,此时对对象进行移动而非拷贝可以提升性能 有些类如IO类或un ...
- [转载]如何在C++03中模拟C++11的右值引用std::move特性
本文摘自: http://adamcavendish.is-programmer.com/posts/38190.htm 引言 众所周知,C++11 的新特性中有一个非常重要的特性,那就是 rvalu ...
- 透彻理解C++11新特性:右值引用、std::move、std::forward
目录 浅拷贝.深拷贝 左值.右值 右值引用类型 强转右值 std::move 重新审视右值引用 右值引用类型和右值的关系 函数参数传递 函数返还值传递 万能引用 引用折叠 完美转发 std::forw ...
- C++ 11中的右值引用以及std::move
看了很多篇文章,现在终于搞懂了C++ 中的右值以及std::move 左值和右值最重要的区别就是右值其实是一个临时的变量 在C++ 11中,也为右值引用增加了新语法,即&& 比 ...
- std::move()和std::forward()
std::move(t)负责将t的类型转换为右值引用,这种功能很有用,可以用在swap中,也可以用来解决完美转发. std::move()的源码如下 template<class _Ty> ...
- C++ 11 右值引用以及std::move
转载请注明出处:http://blog.csdn.net/luotuo44/article/details/46779063 新类型: int和int&是什么?都是类型.int是整数类型,in ...
随机推荐
- Ubuntu 20.04 开启局域网唤醒(WoL)
打开主板相关设置 创建 systemd 自启动设置文件 vim /etc/systemd/system/wol@.service 放入以下内容: [Unit] Description=Wake-on- ...
- Avalonia 实现跨平台的IM即时通讯、语音视频通话(源码,支持信创国产OS,统信、银河麒麟)
在 Avalonia 如火如荼的现在,之前使用CPF实现的简单IM,非常有必要基于 Avalonia 来实现了.Avalonia 在跨平台上的表现非常出色,对信创国产操作系统(像银河麒麟.统信UOS. ...
- 自定义springboot-starter 动态数据源
自定义springboot-starter 动态数据源 如果使用的是spring或springboot框架,spring提供了一个实现动态数据源的一个抽象类AbstractRoutingDataSou ...
- Maven的安装与配置本地仓库,镜像源,环境变量。
参考视频: 黑马程序员2023新版JavaWeb开发教程,实现javaweb企业开发全流程 [小飞非系列]最新Maven实战教程-项目实战构建利器 一.下载Maven安装包 注意安装maven前要先安 ...
- MacOS|matplotlib 无法显示中文 解决办法
matplotlib 无法显示中文 解决办法 画图时,中文无法正常显示,如图 下载字体 点击这里获取字体 提取码: wnby 查看字体路径 在 python 环境中执行以下指令 import matp ...
- [ABC311G] One More Grid Task
Problem Statement There is an $N \times M$ grid, where the square at the $i$-th row from the top and ...
- liunx系统sed命令使用
增 sed '$行数i 新增内容' 文件名在文件里某行上面新增内容 sed '$行数a 新增内容' 文件名在文件里某行下面新增内容 -i.bak对源文件进行修改并备份修改之前源文件 #在文件里第5行上 ...
- Java在指定路径下执行cmd命令的方法
目前状态:毕业设计ing 背景: 做毕设时,由于需要将python的运行效果展示出来,所以使用了Java写了一个前端的界面.但是在使用Java对python的脚本进行调用时就尴尬了,出错-- 这里也许 ...
- 文心一言 VS 讯飞星火 VS chatgpt (160)-- 算法导论12.4 2题
二.用go语言,请描述这样一棵有 n 个结点的二叉搜索树,其树中结点的平均深度为 O(lgn),但这棵树的高度是w(lgn).一棵有 n个结点的二叉搜索树中结点的平均深度为 O(lgn),给出这棵树高 ...
- Javascript Ajax总结——其他跨域技术之Comet
Comet指一种更高级的Ajax技术( 也称 "服务器推送" ),一种服务器向页面推送数据的技术.Comet能够让信息近乎实时地被推送到页面上,非常适合体育比赛的分数和股票报价.有 ...