简单分析lambda实现原理

本文首发于我的公众号:码农手札,主要介绍linux下c++开发的知识包括网络编程的知识同时也会介绍一些有趣的算法题,欢迎大家关注,利用碎片时间学习一些编程知识,冰冻三尺非一日之寒,让我们一起加油!

前言

今天在网上冲浪的时候无意中看到了lambda的表达式,突然想到一个问题,lambda表达式的实现原理是什么呢,今天,我们就来做个简单的分析,了解一下c++在提供lambda表达式背后的实现机制

例子

1
2
3
4
5
6
7
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
};

这段代码的输出结果相当大家都不会觉得奇怪,vec和vec2的size大小都是3,那么接下来看这个例子:

1
2
3
4
5
6
7
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
};

这次相信很多人已经没办法理解了,那么接下来我们来解释一下

move的本质

关于move,我们需要知道的最重要的事情就是move一定会成功吗?
显然不是,让我们来看看move的简易实现代码:

1
2
3
4
5
6
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

从代码可以看出来,move的本质是调用了static_cast做了一层强制转换,强制类型转换的目标类型是remove_reference_t<T>&&,关于remove_reference_t,是为了去除类型本身的引用,比如参数是左值引用。简单来说,move的目的是将对象强制类型转换为右值引用。那么使用move实现移动语义的本质又是什么呢,很简单,本质上就是移动构造函数或者移动赋值运算符,因此,对于下面这种例子:

A a = std::move(b);//假定A定义了移动构造函数

那么就会调用A的移动构造函数来构造a,实现了移动语义。

那么问题来了,什么情况下move会失效呢,其实很简单,如果move之后的变量类型不是&&,那么就不会触发移动构造函数或者移动构造运算符,譬如下面这个例子:

const std::string str = “hello world”;
std::string str2(std::move(str));

这种情况下,对str调用move函数得到的变量类型是const std::string&& ,所以移动构造函数实际上不会起作用,反而会触发拷贝构造函数(由完美转发)

接下来我们来解释为什么两个例子有不同的结果,首先我们需要了解lambda的原理

lambda闭包原理

对于c++的lambda,编译器会将转换为一个匿名的闭包类(闭包简单来理解就是带有上下文,也就是有状态的),所以对于开始例子中给出的一个lambda,实际上转化为下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
///before translation
auto fun = [=](){
auto v2 = std::move(v1);
}

///after translation
class CloseFunc{
public:
void operator()() const{
auto v2 = std::move(v1);
}
private:
vector<int> v1;///or maybe const? but it doesn't matter
};

CloseFunc func;

需要注意的是,生成的闭包类的operator()函数默认被const修饰,也就是说operator()函数是const成员函数,所以对成员变量v1调用std::move的结果就会引发上面所说的move失效的情况,转变出来的类型是const string&&,所以移动构造函数并不会被调用,反而调用的是拷贝构造函数

总结

到这里,大家应该可以理解第一个例子中的情况,在第二个例子中我们引入了mutable,这样的话闭包类生成的operator()函数不会被const修饰,所以我们调用move就会成功匹配到移动构造函数,实现了移动语义

参考文章:
c++闭包理解
c++中mutable关键字
C++中lambda表达式详解与原理分析
C++ lambda 内 std::move 失效问题的思考