C/C++语言中闭包的探究及比较

这里主要讨论的是C语言的扩展特性block。该特性是Apple为C、C++、Objective-C增长的扩展,让这些语言能够用类Lambda表达式的语法来建立闭包。前段时间,在对CoreData存取进行封装时(让开发人员能够更简洁快速地写相关代码),我对block机制有了进一步了解,以为能够和C++ 11中的Lambda表达式相互印证,因此最近从新作了下整理,分享给你们。 html

0. 简单建立匿名函数

下面两段代码的做用都是建立匿名函数并调用,输出Hello, World语句。分别使用Objective-C和C++ 11: 前端

1
^{printf("Hello, World!\n"); } ();
1
[] { cout <<"Hello, World"<< endl; } ();

Lambda表达式的一个好处就是让开发人员能够在须要的时候临时建立函数,便捷。 python

在建立闭包(或者说Lambda函数)的语法上,Objective-C采用的是上尖号^,而C++ 11采用的是配对的方括号[]ios

不过“匿名函数”一词是针对程序员而言的,编译器仍是采起了必定的命名规则。 c++

好比下面Objective-C代码中的3个block, 程序员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>
 
int(^maxBlk)(int,int) = ^(intm,intn){returnm > n ? m : n; };
 
intmain(intargc,constchar* argv[])
{
    ^{printf("Hello, World!\n"); } ();
 
    inti = 1024;
    void(^blk)(void) = ^{printf("%d\n", i); };
    blk();
 
    return0;
}

会产生对应的3个函数: express

1
2
3
__maxBlk_block_func_0
__main_block_func_0
__main_block_func_1

可见函数的命名规则为:__{$Scope}_block_func_{$index}。其中{$Scope}为block所在函数,若是{$Scope}为全局就取block自己的名称;{$index}表示该block在{$Scope}做用域内出现的顺序(第几个block)。 编程

1. 从语法上看如何捕获外部变量

在上面的代码中,已经看到“匿名函数”能够直接访问外围做用域的变量i: bash

1
2
3
inti = 1024;
void(^blk)(void) = ^{printf("%d\n", i); };
blk();

当匿名函数和non-local变量结合起来,就造成了闭包(我的见解)。
这一段代码能够成功输出i的值。 闭包

咱们把同样的逻辑搬到C++上:

1
2
3
inti = 1024;
auto func = [] {printf("%d\n", i); };
func();

GCC会输出:错误:‘i’未被捕获。可见在C++中没法直接捕获外围做用域的变量。

以BNF来表示Lambda表达式的上下文无关文法,存在:

1
2
lambda-expression : lambda-introducer lambda-parameter-declarationopt compound-statement
lambda-introducer : [ lambda-captureopt ]

所以,方括号中还能够加入一些选项:

1
2
3
4
5
6
[]        Capture nothing (or, a scorched earth strategy?)
[&]       Capture any referenced variable by reference
[=]       Capture any referenced variable by making a copy
[=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference
[bar]     Capture bar by making a copy; don't copy anythingelse
[this]    Capture thethispointer of the enclosingclass

根据文法,对代码加以修改,使其可以成功运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash-3.2# vi testLambda.cpp
bash-3.2# g++-4.7 -std=c++11 testLambda.cpp -o testLambda
bash-3.2# ./testLambda
1024
bash-3.2# cat testLambda.cpp
#include <iostream>
 
using namespacestd;
 
intmain()
{
     inti = 1024;
     auto func = [=] {printf("%d\n", i); };
     func();
 
     return0;
}
bash-3.2#

2. 从语法上看如何修改外部变量

上面代码中使用了符号=,经过拷贝方式捕获了外部变量i。
可是若是尝试在Lambda表达式中修改变量i:

1
auto func = [=] { i = 0;printf("%d\n", i); };

会获得错误:

1
2
testLambda.cpp: 在 lambda 函数中:
testLambda.cpp:9:24: 错误:向只读变量‘i’赋值

可见经过拷贝方式捕获的外部变量是只读的。Python中也有一个相似的经典case,我的以为有相通之处:

1
2
3
4
5
x=10
deffoo():
    print(x)
    x+=1
foo()

这段代码会抛出UnboundLocalError错误,缘由能够参见FAQ

在C++的闭包语法中,若是须要对外部变量的写权限,可使用符号&,经过引用方式捕获:

1
2
3
inti = 1024;
auto func = [&] { i = 0;printf("%d\n", i); };
func();

反过来,将修改外部变量的逻辑放到Objective-C代码中:

1
2
3
inti = 1024;
void(^blk)(void) = ^{ i = 0;printf("%d\n", i); };
blk();

会获得以下错误:

1
2
3
4
main.m:14:29: error: variable is not assignable (missing __block type specifier)
    void(^blk)(void) = ^{ i++;printf("%d\n", i); };
                           ~^
1 error generated.

可见在block的语法中,默认捕获的外部变量也是只读的,若是要修改外部变量,须要使用__block类型指示符进行修饰。
为何呢?请继续往下看 :)

3. 从实现上看如何捕获外部变量

闭包对于编程语言来讲是一种语法糖,包括Block和Lambda,是为了方便程序员开发而引入的。所以,对Block特性的支持会落地在编译器前端,中间代码将会是C语言。

先看以下代码会产生怎样的中间代码。

1
2
3
4
5
6
7
8
intmain(intargc,constchar* argv[])
{
    inti = 1024;
    void(^blk)(void) = ^{printf("%d\n", i); };
    blk();
 
    return0;
}

首先是block结构体的实现:

1
2
3
4
5
6
7
8
9
10
11
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct__block_impl {
    void*isa;
    intFlags;
    intReserved;
    void*FuncPtr;
};
// 省略部分代码
 
#endif

第一个成员isa指针用来表示该结构体的类型,使其仍然处于Cocoa的对象体系中,相似Python对象系统中的PyObject。

第2、三个成员是标志位和保留位。

第四个成员是对应的“匿名函数”,在这个例子中对应函数:

1
2
3
4
staticvoid__main_block_func_0(struct__main_block_impl_0 *__cself) {
    inti = __cself->i;// bound by copy
    printf("%d\n", i);
}

函数__main_block_func_0引入了参数__cself,为struct __main_block_impl_0 *类型,从参数名称就能够看出它的功能相似于C++中的this指针或者Objective-C的self。
而struct __main_block_impl_0的结构以下:

1
2
3
4
5
6
7
8
9
10
11
struct__main_block_impl_0 {
    struct__block_impl impl;
    struct__main_block_desc_0* Desc;
    inti;
    __main_block_impl_0(void*fp,struct__main_block_desc_0 *desc,int_i,intflags=0) : i(_i) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

从__main_block_impl_0这个名称能够看出该结构体是为main函数中第零个block服务的,即示例代码中的blk;也能够猜到不一样场景下的block对应的结构体不一样,但本质上第一个成员必定是struct __block_impl impl,由于这个成员是block实现的基石。

结构体__main_block_impl_0又引入了一个新的结构体,也是中间代码里最后一个结构体:

1
2
3
4
staticstruct__main_block_desc_0 {
    unsignedlongreserved;
    unsignedlongBlock_size;
} __main_block_desc_0_DATA = { 0,sizeof(struct__main_block_impl_0)};

能够看出,这个描述性质的结构体包含的价值信息就是struct __main_block_impl_0的大小。

最后剩下main函数对应的中间代码:

1
2
3
4
5
6
7
8
intmain(intargc,constchar* argv[])
{
    inti = 1024;
    void(*blk)(void) = (void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, i);
    ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk);
 
    return0;
}

从main函数对应的中间代码能够看出执行block的本质就是以block结构体自身做为__cself参数,这里对应__main_block_impl_0,经过结构体成员FuncPtr函数指针调用对应的函数,这里对应__main_block_func_0。

其中,局部变量i是以值传递的方式拷贝一份,做为__main_block_impl_0的构造函数的参数,并以初始化列表的形式赋值给其成员变量i。因此,基于这样的实现,不容许直接修改外部变量是合理的——由于按值传递根本改不到外部变量。

4. 从实现上看如何修改外部变量(__block类型指示符)

若是想要修改外部变量,则须要用__block来修饰:

1
2
3
4
5
6
7
8
intmain(intargc,constchar* argv[])
{
    __blockinti = 1024;
    void(^blk)(void) = ^{ i = 0;printf("%d\n", i); };
    blk();
 
    return0;
}

此时再看中间代码,发现多了一个结构体:

1
2
3
4
5
6
7
struct__Block_byref_i_0 {
    void*__isa;
    __Block_byref_i_0 *__forwarding;
    int__flags;
    int__size;
    inti;
};

因而,用__block修饰的int变量i化身为__Block_byref_i_0结构体的最后一个成员变量

代码中blk对应的结构体也发生了变化:

1
2
3
4
5
6
7
8
9
10
11
struct__main_block_impl_0 {
    struct__block_impl impl;
    struct__main_block_desc_0* Desc;
    __Block_byref_i_0 *i;// by ref
    __main_block_impl_0(void*fp, struct__main_block_desc_0 *desc, __Block_byref_i_0 *_i,intflags=0) : i(_i->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

__main_block_impl_0发生的变化就是int类型的成员变量i换成了__Block_byref_i_0 *类型,从名称能够看出如今要经过引用方式来捕获了。

对应的函数也不一样了:

1
2
3
4
5
staticvoid__main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_i_0 *i = __cself->i;// bound by ref
    (i->__forwarding->i) = 0;// 看起来很厉害的样子
    printf("%d\n", (i->__forwarding->i));
}

main函数也有了变更:

1
2
3
4
5
6
7
8
intmain(intargc,constchar* argv[])
{
    __block __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0,sizeof(__Block_byref_i_0), 1024};
    void(*blk)(void) = (void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (struct__Block_byref_i_0 *)&i, 570425344);
    ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk);
 
    return0;
}

前两行代码建立了两个关键结构体,特意高亮显示。

这里没有看__main_block_desc_0发生的变化,放到后面讨论

使用__block类型指示符的本质就是引入了__Block_byref_{$var_name}_{$index}结构体,而被__block关键字修饰的变量就被放到这个结构体中。另外,block结构体经过引入__Block_byref_{$var_name}_{$index}指针类型的成员,得以间接访问到外部变量。

经过这样的设计,咱们就能够修改外部做用域的变量了,再一次应了那句话:

There is no problem in computer science that can’t be solved by adding another level of indirection.

指针是咱们最常用的间接手段,而这里的本质也是经过指针来间接访问,为何要特意引入__Block_byref_{$var_name}_{$index}结构体,而不是直接使用int *来访问外部变量i呢?

另外,__Block_byref_{$var_name}_{$index}结构体中的__forwarding指针成员有何做用?

请继续往下看 :)

5. 背后的内存管理动做

在Objective-C中,block特性的引入是为了让程序员能够更简洁优雅地编写并发代码(配合看起来像敏感词的GCD)。比较常见的就是将block做为函数参数传递,以供后续回调执行。

先看一段完整的、可执行的代码:

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
#import <Foundation/Foundation.h>
#include <pthread.h>
 
typedefvoid(^DemoBlock)(void);
 
voidtest();
void*testBlock(void*blk);
 
intmain(intargc,constchar* argv[])
{
    printf("Before test()\n");
    test();
    printf("After test()\n");
 
    sleep(5);
    return0;
}
 
voidtest()
{
    __blockinti = 1024;
    void(^blk)(void) = ^{ i = 2048;printf("%d\n", i); };
 
    pthread_tthread;
    intret = pthread_create(&thread, NULL, testBlock, (void*)blk);
    printf("thread returns : %d\n", ret);
 
    sleep(3);// 这里睡眠1s的话,程序会崩溃
}
 
void*testBlock(void*blk)
{
    sleep(2);
 
    printf("testBlock : Begin to exec blk.\n");
    DemoBlock demoBlk = (DemoBlock)blk;
    demoBlk();
 
    returnNULL;
}

在这个示例中,位于test()函数的block类型的变量blk就做为函数参数传递给testBlock。

正常状况下,这段代码能够成功运行,输出:

1
2
3
4
5
Before test()
threadreturns : 0
testBlock : Begin to exec blk.
2048
After test()

若是按照注释,将test()函数最后一行改成休眠1s的话,正常状况下程序会在输出以下结果后崩溃:

1
2
3
4
Before test()
threadreturns : 0
After test()
testBlock : Begin to exec blk.

从输出能够看出,当要执行blk的时候,test()已经执行完毕回到main函数中,对应的函数栈也已经展开,此时栈上的变量已经不存在了,继续访问致使崩溃——这也是不用int *直接访问外部变量i的缘由。

5.1 拷贝block结构体

上文提到block结构体__block_impl的第一个成员是isa指针,使其成为NSObject的子类,因此咱们能够经过相应的内存管理机制将其拷贝到堆上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
voidtest()
{
    __blockinti = 1024;
    void(^blk)(void) = ^{ i = 2048;printf("%d\n", i); };
 
    pthread_tthread;
    intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]);
    printf("thread returns : %d\n", ret);
 
    sleep(1);
}
 
void*testBlock(void*blk)
{
    sleep(2);
 
    printf("testBlock : Begin to exec blk.\n");
    DemoBlock demoBlk = (DemoBlock)blk;
    demoBlk();
    [demoBlk release];
 
    returnNULL;
}

再次执行,获得输出:

1
2
3
4
5
Before test()
threadreturns : 0
After test()
testBlock : Begin to exec blk.
2048

能够看出,在test()函数栈展开后,demoBlk仍然能够成功执行,这是因为blk对应的block结构体__main_block_impl_0已经在堆上了。不过这还不够——

5.2 拷贝捕获的变量(__block变量)

在拷贝block结构体的同时,还会将捕获的__block变量,即结构体__Block_byref_i_0,复制到堆上。这个任务落在前面没有讨论的__main_block_desc_0结构体身上:

1
2
3
4
5
6
7
8
9
10
staticvoid__main_block_copy_0(struct__main_block_impl_0*dst,struct__main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
 
staticvoid__main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
 
staticstruct__main_block_desc_0 {
    unsignedlongreserved;
    unsignedlongBlock_size;
    void(*copy)(struct__main_block_impl_0*,struct__main_block_impl_0*);
    void(*dispose)(struct__main_block_impl_0*);
} __main_block_desc_0_DATA = { 0,sizeof(struct__main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

栈上的__main_block_impl_0结构体为src,堆上的__main_block_impl_0结构体为dst,当发生复制动做 时,__main_block_copy_0函数会获得调用,将src的成员变量i,即__Block_byref_i_0结构体,也复制到堆上

5.3 __forwarding指针的做用

当复制动做完成后,栈上和堆上都存在着__main_block_impl_0结构体。若是栈上、堆上的block结构体都对捕获的外部变量进行操做,会如何?

下面是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
voidtest()
{
    __blockinti = 1024;
    void(^blk)(void) = ^{ i++;printf("%d\n", i); };
 
    pthread_tthread;
    intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]);
    printf("thread returns : %d\n", ret);
 
    sleep(1);
    blk();
}
 
void*testBlock(void*blk)
{
    sleep(2);
 
    printf("testBlock : Begin to exec blk.\n");
    DemoBlock demoBlk = (DemoBlock)blk;
    demoBlk();
    [demoBlk release];
 
    returnNULL;
}
  1. 在test()函数中调用pthread_create建立线程时,blk被复制了一份到堆上做为testBlock函数的参数。
  2. test()函数中的blk结构体位于栈中,在休眠1s后被执行,对i进行自增动做。
  3. testBlock函数在休眠2s后,执行位于堆上的block结构体,这里为demoBlk。

上述代码执行后输出:

1
2
3
4
5
6
Beforetest()
thread returns : 0
1025
Aftertest()
testBlock : Begin toexecblk.
1026

可见不管是栈上的仍是堆上的block结构体,修改的都是同一个__block变量

这就是前面提到的__forwarding指针成员的做用了:

起初,栈上的__block变量的成员指针__forwarding指向__block变量自己,即栈上的__Block_byref_i_0结构体。

当__block变量被复制到堆上后,栈上的__block变量的__forwarding成员会指向堆上的那一份拷贝,从而保持一致。

参考资料:

相关文章
相关标签/搜索