众所周知,Lambda函数是C++11带来的一个很重要的特性。

考虑到并不是所有Lambda函数都是就地使用的,我们需要将它“存下来”。从一个Lambda函数生成一个带捕获变量的闭包后,有两种通常的存储方法。

第一种,最基本的方法,使用auto声明一个自动推导类型的变量:

1 long a = 1;
2 
3 auto func1 = [=] (long b) {
4     return a + b;
5 };

这样做的一个缺点是,一旦func1离开了它的作用域,就会被自动回收。

第二种,也是很多资料中所称的“更强大”的方法,使用std::function模板来接收:

1 long a = 1;
2 
3 std::function<long (long)> func2 = [=] (long b) {
4     return a + b;
5 };

我们可以分别计算sizeof(func1)sizeof(func2),发现前者就是Lambda函数捕获的变量的大小,而后者是一个固定值。

现在问题来了:

  • 那个auto代表什么类型?能不能直接写出来?
  • func1的大小具体是怎么决定的?
  • std::function干了什么?捕获的变量去哪儿了?
  • 捕获变量(或者说闭包)的生存期可以比上面例子中的更长吗?

下面,我们来解答这些问题。

Lambda的类型

C++的Lambda是一种典型的语法糖。例如上文出现的这个Lambda函数:

1 auto func1 = [=] (long b) {
2     return a + b;
3 };

会被翻译成:

 1 class SomeAnonymousType {
 2 private:
 3     long a;
 4 public:
 5     SomeAnonymousType(long init_a): a(init_a) {}
 6 
 7     long operator() (long b) const {
 8         return a + b;
 9     }
10 };
11 
12 auto func1 = SomeAnonymousType(a);

也就是说,C++11中的匿名函数相当于一个匿名的函数对象。

=捕获的变量会直接成为这个函数对象的成员;如果捕获方式是&(按引用捕获),函数对象中将会存储一个指针。

第二个问题也一起解答了,我们把匿名函数翻译成像SomeAnonymousType这样的函数对象,根据C++的内存对齐方式,就可以算出大小。另外,我用clang++测试了一下,编译器不会对变量的捕获顺序做特别的改变。这也就意味着,如果我们交替地捕获不同大小的变量,可能会浪费一些额外的空间。

长期存储

不难猜到,std::function把闭包存储在了堆中,所以它本身的大小是固定的。既然匿名函数是一种语法糖,我们当然可以剥开糖衣,按照它的本来面目使用它——像处理一个函数对象那样处理闭包——那么,闭包存储在堆中也应当是可行的。

我们可以自己动手,实现一个简单的Lambda容器,作为std::function的替代品。

首先,我们需要设计这样一个类,它接收一个变量(可选择通过copy或者move接收),然后从堆中分配一段内存来存储它。

一个办法是通过模板。我们需要“记住”变量的类型,来完成释放内存等必要的操作。因此我们引入了一个Helper类,把操作实例化,再用函数指针记下。

顺带一提:这里也可以使用Lambda函数,读者如有兴趣,不妨尝试一下。

 1 // using namespace std;
 2 
 3 class Container {
 4 private:
 5     void *buf;
 6 
 7     template <class T>
 8     struct Helper {
 9         static void del(void *ptr) {
10             delete (T *) ptr;
11         }
12     };
13 
14     void (*del)(void *ptr);
15 
16 public:
17     template <class T>
18     Container(T &data) {
19         buf = (void *) new T(data);
20         del = Helper<T>::del;
21 
22         cout << "copy" << endl;
23     }
24 
25     template <class T>
26     Container(T &&data) {
27         buf = (void *) new T(data);
28         del = Helper<T>::del;
29 
30         cout << "move" << endl;
31     }
32 
33     ~Container() {
34         del(buf);
35 
36         cout << "free" << endl;
37     }
38 };
39 
40 // usage:
41 
42 double pi = 3.14;
43 
44 Container x(1);
45 Container y(3.14);
46 Container z(pi);

然后,我们像std::function那样,把Container改成一个模板,加上函数调用的参数、返回值的类型。

为了让它能被调用,我们加上一个operator(),把它变成一个函数对象。我们同样利用Helper类:添加一个call()成员,将调用转发给Lambda函数:

 1 // using namespace std;
 2 
 3 template <class out, class... in>
 4 class LambdaContainer {
 5 private:
 6     void *buf;
 7 
 8     template <class T>
 9     struct Helper {
10         static out call(void *ptr, in... arg) {
11             return ((T *) ptr)->operator()(std::forward<in...>(arg...));
12         }
13 
14         static void del(void *ptr) {
15             delete (T *) ptr;
16         }
17     };
18 
19     out (*call)(void *ptr, in... arg);
20     void (*del)(void *ptr);
21 
22 public:
23     template <class T>
24     LambdaContainer(T &data) {
25         buf = (void *) new T(data);
26         call = Helper<T>::call;
27         del = Helper<T>::del;
28 
29         cout << "copy" << endl;
30     }
31 
32     template <class T>
33     LambdaContainer(T &&data) {
34         buf = (void *) new T(data);
35         call = Helper<T>::call;
36         del = Helper<T>::del;
37 
38         cout << "move" << endl;
39     }
40 
41     out operator()(in... arg) {
42         return call(buf, std::forward<in...>(arg...));
43     }
44 
45     ~LambdaContainer() {
46         del(buf);
47 
48         cout << "free" << endl;
49     }
50 };
51 
52 // usage:
53 
54 double pi = 3.14;
55 
56 LambdaContainer<double, double> func(
57     [=](double n){
58         return n * pi;
59     }
60 );
61 
62 cout << func(5) << endl;

至此,一个简单但初步可用的Lambda容器就完成了。当然,为了让它更完整,我们还应该让它能判断buf是否为空、能被copy或者move,甚至可以考虑支持引用计数。我们可以用它保存Lambda函数,直到天荒地老释放为止。