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

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

感谢投稿人 @思禽饮霜 

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

0. 简单创建匿名函数

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

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

Lambda表达式的一个好处就是让开发人员可以在需要的时候临时创建函数,便捷。

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

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

比如下面Objective-C代码中的3个block,

#import <Foundation/Foundation.h>

int (^maxBlk)(int , int) = ^(int m, int n){ return m > n ? m : n; };

int main(int argc, const char * argv[])
{
    ^{ printf("Hello, World!\n"); } ();

    int i = 1024;
    void (^blk)(void) = ^{ printf("%d\n", i); };
    blk();

    return 0;
}

会产生对应的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:

int i = 1024;
void (^blk)(void) = ^{ printf("%d\n", i); };
blk();

当匿名函数和non-local变量结合起来,就形成了闭包(个人看法)。
这一段代码可以成功输出i的值。

我们把一样的逻辑搬到C++上:

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

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

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

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

因此,方括号中还可以加入一些选项:

[]        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 anything else
[this]    Capture the this pointer of the enclosing class

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

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  namespace std;

int main()
{
     int i = 1024;
     auto func = [=] { printf("%d\n", i); };
     func();

     return 0;
}
bash-3.2#

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

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

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

会得到错误:

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

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