这里整理一些关于 c++ 中的匿名函数的知识.
<< C++ primer >> 上有的内容就不在重述了. 这里重点讲一些 primer 上 可能 没有的东西.
捕获的时机
primer 向我们介绍到: 有两种捕获变量的方式, 值捕获和引用捕获. 其中:
…
与参数不同, 被捕获的变量的值是在 lambda 创建时拷贝, 而不是调用时拷贝
…
如果我们采用引用方式捕获一个变量, 就必须确保被引用的对象在 lambda 执行的时候是存在的. lambda捕获的都是局部变量, 这些变量在函数结束后就不复存在了.
…
我们看一段代码:
auto funtionTimesMod1(int mod) {
int variableA = mod;
auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
return f;
}
void test1() {
cout << "---test1---" << endl;
int a = 10, b = 20, c = 7;
auto times = funtionTimesMod1(c);
cout << times(a, b) << endl;
}
这里 funtionTimesMod1(int mod)
返回一个签名为 int(int, int)
的函数, 这个函数计算两个参数相乘对 mod
取模的结果.
当然可以正常运行, 结果为 4
, 如果我们将它换成引用捕获:
auto funtionTimesMod2(int mod) {
int variableA = mod;
auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
return f;
}
void test2() {
cout << "---test2---" << endl;
int a = 10, b = 20, c = 7;
auto times = funtionTimesMod2(c);
cout << times(a, b) << endl;
}
这段代码就已经不能正常工作了, 它的输出是 0
.
我们来看看为啥:
Process 15820 stopped
* thread #1, name = 'tmp', stop reason = step in
frame #0: 0x000000000040096b tmp`funtionTimesMod2(mod=7) at tmp.cpp:17
14 }
15
16 auto funtionTimesMod2(int mod) {
-> 17 int variableA = mod;
18 auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
19 return f;
20 }
(lldb) p &variableA
(int *) $1 = 0x00007fffffffe120
Process 15820 stopped
* thread #1, name = 'tmp', stop reason = step over
frame #0: 0x00000000004009dc tmp`test2() at tmp.cpp:25
22 cout << "---test2---" << endl;
23 int a = 10, b = 20, c = 7;
24 auto times = funtionTimesMod2(c);
-> 25 cout << times(a, b) << endl;
26 }
27 auto funtionTimesMod3(int mod) {
28 int variableA = mod;
(lldb) p times
((anonymous class)) $2 = {
variableA = 0x00007fffffffe120
}
可以看到 times
这个对象中保存下来的 variableA
只是一个指针, 它指向我们之前创建的局部变量 variableA
, 这个地址在栈上, 这意味着当我们真的调用 times
的时候, 局部变量 variableA
所在的那片内存已经被使用过了. 所以会返回错误的结果. 接下来, 我们可以看到实际上调用的时候 variableA
里面是 20
, 这是因为刚好参数 b
被放置在了 variableA
之前的位置上.
Process 21271 stopped
* thread #1, name = 'tmp', stop reason = step in
frame #0: 0x0000000000400a32 tmp`funtionTimesMod2(this=0x00007fffffffe158, a=10, b=20)::$_1::operator()(int, int) const at tmp.cpp:18
15
16 auto funtionTimesMod2(int mod) {
17 int variableA = mod;
-> 18 auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
19 return f;
20 }
21 void test2() {
(lldb) p variableA
(int) $3 = 20
(lldb) p &b
(int *) $4 = 0x00007fffffffe120
lambda 的实现
根据 C++ Primer 在 10.3 以及 14.8.1 的介绍, 我们知道一个 lambda 表达式是由编译器负责翻译成一个 没有类名, 重载过调用运算符 的对象的.
这解释了为什么我们只能用 auto 来定义一个 lambda 类型的变量, 因为这个类是没有名字的, 或者说它的名字是编译器自己生成的, 我们并不能知道它叫什么.
这听起来很美好, 我们也确实可以写出代码, 用一个自己创建的, 重载过调用运算符的对象, 来模拟一个 lambda:
class INT {
public:
int num;
INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
INT() = default;
};
auto funtionTimesMod3(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
auto f = [variableA](int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
};
cout << "----D" << endl;
return f;
}
void test3() {
cout << "---test3---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod3(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
auto funtionTimesMod4(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
class function {
const INT variableA;
public:
function(const INT &variableA) : variableA(variableA) {}
int operator()(int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
}
} f(variableA);
cout << "----D" << endl;
return f;
}
void test4() {
cout << "---test4---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod4(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
为了观察发生的值拷贝的时机, 以确定我们自己写的这个类的行为和 lambda 真的完全一致, 我们可以自定义一个class, 并且让他发生拷贝构造的时候打印一些信息.
运行结果如下:
---test3---
----E
----A
----B
Copy constructor
----D
----F
----C
4
----G
---test4---
----E
----A
----B
Copy constructor
----D
----F
----C
4
----G
这又一次说明了说明捕获引起的值拷贝发生在 lambda 被初始化的时候.
看起来这两个东西的行为几乎一致, 但是真的是这样吗?
编译器的优化
编译器会对我们写的代码做出一些优化, 以减少复制对象的次数. 如果不了解这点可以看看 这篇 博客.
如果我们关闭返回值优化, 那么运行的结果是这样的:
---test3---
----E
----A
----B
Copy constructor
Copy constructor
----D
Copy constructor
Copy constructor
----F
----C
4
----G
---test4---
----E
----A
----B
Copy constructor
----D
Copy constructor
Copy constructor
----F
----C
4
----G
可以看到我们自己写的类, 少了一次拷贝构造.
我们先来解释一下 lambda 为什么会发生这么多次拷贝构造.
auto funtionTimesMod3(int mod) {
INT variableA; // 5
cout << "----A" << endl; // 6
variableA.num = mod; // 7
cout << "----B" << endl; // 8
auto f = [variableA](int a, int b) {
cout << "----C" << endl; // 14
return a % variableA.num * b % variableA.num; //15
}; // 9
cout << "----D" << endl; // 10
return f; // 11
}
void test3() {
cout << "---test3---" << endl; // 1
int a = 10, b = 20, c = 7; // 2
cout << "----E" << endl; // 3
auto times = funtionTimesMod3(c); // 4
cout << "----F" << endl; // 12
cout << times(a, b) << endl; // 13
cout << "----G" << endl; //16
}
首先我们的程序是按照如上的顺序运行的.
可以看到前两个拷贝构造发生在 9, 而第 3, 4 次发生在 11.
如果完全按照语义来看的话, 9 这句话可以有两种理解方式:
- 创建一个 lambda 对象, 对象名字叫 f, 这个对象的内容就是后面那个 lambda 表达式.
- 创建一个 lambda 表达式, 然后将其作为参数, 调用同类型对象 f 的拷贝构造函数.
如果按照第一种方法来理解, 那么第 9 行发生两次拷贝构造就不是很能理解了.
所以应该是第二种.
在 10 之后也发生了两次 拷贝调用. 应该是先建立了一个变量用来做返回值, 比如说叫 r, 然后将 f 赋值给 返回值变量r, 然后返回值变量 r 在被赋值给test3() 中的 times, 这样发生的两次拷贝构造.
auto funtionTimesMod4(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
class function {
const INT variableA;
public:
function(const INT &variableA) : variableA(variableA) {}
int operator()(int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
}
} f(variableA); // <- 这里
cout << "----D" << endl;
return f;
}
void test4() {
cout << "---test4---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod4(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
之前自己模拟 lambda 的这个代码, 我们在注释标注的那个位置的实现和 lambda 中, “先建一个右值, 然后拷贝构造出f” 的行为不太一样. 导致这里少了一次拷贝构造. 所以实际上应该是这样:
auto funtionTimesMod5(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
class function {
const INT variableA;
public:
function(const INT &variableA) : variableA(variableA) {}
int operator()(int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
}
};
function f = function(variableA);
cout << "----D" << endl;
return f;
}
void test5() {
cout << "---test5---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod5(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
实际上没必要纠结那么多, 正常编译的话, 其实是不会去先建一个右值的对象的.
代码
#include <bits/stdc++.h>
using namespace std;
auto funtionTimesMod1(int mod) {
int variableA = mod;
auto f = [variableA](int a, int b) { return a % variableA * b % variableA; };
return f;
}
void test1() {
cout << "---test1---" << endl;
int a = 10, b = 20, c = 7;
auto times = funtionTimesMod1(c);
cout << times(a, b) << endl;
}
auto funtionTimesMod2(int mod) {
int variableA = mod;
auto f = [&variableA](int a, int b) { return a % variableA * b % variableA; };
return f;
}
void test2() {
cout << "---test2---" << endl;
int a = 10, b = 20, c = 7;
auto times = funtionTimesMod2(c);
cout << times(a, b) << endl;
}
class INT {
public:
int num;
INT(const INT &i) : num(i.num) { cout << "Copy constructor" << endl; }
INT() = default;
};
auto funtionTimesMod3(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
auto f = [variableA](int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
};
cout << "----D" << endl;
return f;
}
void test3() {
cout << "---test3---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod3(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
auto funtionTimesMod4(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
class function {
const INT variableA;
public:
function(const INT &variableA) : variableA(variableA) {}
int operator()(int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
}
} f(variableA);
cout << "----D" << endl;
return f;
}
void test4() {
cout << "---test4---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod4(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
auto funtionTimesMod5(int mod) {
INT variableA;
cout << "----A" << endl;
variableA.num = mod;
cout << "----B" << endl;
class function {
const INT variableA;
public:
function(const INT &variableA) : variableA(variableA) {}
int operator()(int a, int b) {
cout << "----C" << endl;
return a % variableA.num * b % variableA.num;
}
};
function f = function(variableA);
cout << "----D" << endl;
return f;
}
void test5() {
cout << "---test5---" << endl;
int a = 10, b = 20, c = 7;
cout << "----E" << endl;
auto times = funtionTimesMod5(c);
cout << "----F" << endl;
cout << times(a, b) << endl;
cout << "----G" << endl;
}
int main() {
test1();
test2();
test3();
test4();
test5();
}