lambda在编译器中实现的方式

在C++中lambda-expression的结果叫做闭包对象(closure object)。本篇文章并非是介绍C++ lambda的用法的(这一点《TC++PL》、《C++ Primer》中都十分详细,或者看我之前的总结C++11的语法糖#lambda表达式),而是从LLVM-IR来分析在Clang中是如何实现lambda-expression的。

C++标准中是这么描述lambda的:

[ISO/IEC 14882:2014 §5.1.2.2]The evaluation of a lambda-expression results in a prvalue temporary (12.2). This temporary is called the closure object.
The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type — called the closure type — whose properties are described below. This class type is neither an aggregate (8.5.1) nor a literal type (3.9).
A closure object behaves like a function object (20.9).

标准中提到了闭包类型是一个独一无二的非union类类型。来看一下Clang中对于lambda的实现:

1
2
3
4
5
6
int main(){
int x=123;
double y=456;
auto example=[&](int z)mutable{x=567;y=789;z=666;};
example(111);
}

上面的代码是一个lambda对象捕获了一个int类型与double类型对象并且接受一个int型参数,来看下其LLVM-IR代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
%class.anon = type { i32*, double* }
; Function Attrs: norecurse uwtable
define i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca double, align 8
%3 = alloca %class.anon, align 8
store i32 123, i32* %1, align 4
store double 4.560000e+02, double* %2, align 8
%4 = getelementptr inbounds %class.anon, %class.anon* %3, i32 0, i32 0
store i32* %1, i32** %4, align 8
%5 = getelementptr inbounds %class.anon, %class.anon* %3, i32 0, i32 1
store double* %2, double** %5, align 8
call void @"_ZZ4mainEN3$_0clEi"(%class.anon* %3, i32 111)
ret i32 0
}
; Function Attrs: inlinehint nounwind uwtable
define internal void @"_ZZ4mainEN3$_0clEi"(%class.anon*, i32) #5 align 2 {
%3 = alloca %class.anon*, align 8
%4 = alloca i32, align 4
store %class.anon* %0, %class.anon** %3, align 8
store i32 %1, i32* %4, align 4
%5 = load %class.anon*, %class.anon** %3, align 8
%6 = getelementptr inbounds %class.anon, %class.anon* %5, i32 0, i32 0
%7 = load i32*, i32** %6, align 8
store i32 567, i32* %7, align 4
%8 = getelementptr inbounds %class.anon, %class.anon* %5, i32 0, i32 1
%9 = load double*, double** %8, align 8
store double 7.890000e+02, double* %9, align 8
store i32 666, i32* %4, align 4
ret void
}

这里有几个需要重点关注的部分:

1
2
3
4
5
6
%class.anon = type { i32*, double* }
%3 = alloca %class.anon, align 8
call void @"_ZZ4mainEN3$_0clEi"(%class.anon* %3, i32 111)
define internal void @"_ZZ4mainEN3$_0clEi"(%class.anon*, i32) #5 align 2

可以看到LLVM中lambda的实现就是一个重载了operator()的匿名类类型对象,其中的捕获到的参数都作为了这个类的数据成员,而调用接收的参数是该operator()所接收的参数。
下面看一下我手写一个与上面lambda表达式实现相同功能的函数对象(重载了operator()的类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
public:
A(int &a,double &b):x(a),y(b){}
void operator()(int z){
x=567;y=789;z=666;
}
private:
int &x;
double &y;
};
int main(){
int x=123;
double y=456;
A example(x,y);
example(111);
}

其LLVM-IR代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
%class.A = type { i32*, double* }
; Function Attrs: norecurse uwtable
define i32 @main() #4 {
%1 = alloca i32, align 4
%2 = alloca double, align 8
%3 = alloca %class.A, align 8
store i32 123, i32* %1, align 4
store double 4.560000e+02, double* %2, align 8
call void @_ZN1AC2ERiRd(%class.A* %3, i32* dereferenceable(4) %1, double* dereferenceable(8) %2)
call void @_ZN1AclEi(%class.A* %3, i32 111)
ret i32 0
}
; Function Attrs: nounwind uwtable
define linkonce_odr void @_ZN1AC2ERiRd(%class.A*, i32* dereferenceable(4), double* dereferenceable(8)) unnamed_addr #5 comdat align 2 {
%4 = alloca %class.A*, align 8
%5 = alloca i32*, align 8
%6 = alloca double*, align 8
store %class.A* %0, %class.A** %4, align 8
store i32* %1, i32** %5, align 8
store double* %2, double** %6, align 8
%7 = load %class.A*, %class.A** %4, align 8
%8 = getelementptr inbounds %class.A, %class.A* %7, i32 0, i32 0
%9 = load i32*, i32** %5, align 8
store i32* %9, i32** %8, align 8
%10 = getelementptr inbounds %class.A, %class.A* %7, i32 0, i32 1
%11 = load double*, double** %6, align 8
store double* %11, double** %10, align 8
ret void
}
; Function Attrs: nounwind uwtable
define linkonce_odr void @_ZN1AclEi(%class.A*, i32) #5 comdat align 2 {
%3 = alloca %class.A*, align 8
%4 = alloca i32, align 4
store %class.A* %0, %class.A** %3, align 8
store i32 %1, i32* %4, align 4
%5 = load %class.A*, %class.A** %3, align 8
%6 = getelementptr inbounds %class.A, %class.A* %5, i32 0, i32 0
%7 = load i32*, i32** %6, align 8
store i32 567, i32* %7, align 4
%8 = getelementptr inbounds %class.A, %class.A* %5, i32 0, i32 1
%9 = load double*, double** %8, align 8
store double 7.890000e+02, double* %9, align 8
store i32 666, i32* %4, align 4
ret void
}

来对比下我上面提到的三个关键部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# lambda
%class.anon = type { i32*, double* }
# function object
%class.A = type { i32*, double* }
# lambda
%3 = alloca %class.anon, align 8
call void @"_ZZ4mainEN3$_0clEi"(%class.anon* %3, i32 111)
# function object
%3 = alloca %class.A, align 8
call void @_ZN1AclEi(%class.A* %3, i32 111)
# lambda
define internal void @"_ZZ4mainEN3$_0clEi"(%class.anon*, i32) #5 align 2
# function object
define linkonce_odr void @_ZN1AclEi(%class.A*, i32) #5 comdat align 2

可以看到,我们手写的函数对象与lambda表达式由编译器生成的一模一样...
被捕获列表捕获的对象是作为该编译器生成类的数据成员存放的,而接收参数是作为operator()的参数获得的,上面IR代码中lambda与我手写的最大的区别是,lambda并不会生成相应的构造函数。
由上可知,所以在Clang中,lambda就是以函数对象的方式实现的...
另外,有了lambda再组合STL中的<functional>,简直超强屠龙技啊。

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:lambda在编译器中实现的方式
文章作者:ZhaLiPeng
发布时间:2017年05月17日 23时35分
本文字数:本文一共有1,203字
更新历史: Blame, History  文本模式: .md Raw
原始链接:https://imzlp.me/posts/19441/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!