Skip to content

Commit 12a3f55

Browse files
committed
update lambda
1 parent 4e6b8d9 commit 12a3f55

File tree

3 files changed

+321
-9
lines changed

3 files changed

+321
-9
lines changed

docs/img/thanks.png

9.48 KB
Loading

docs/lambda.md

+317-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 函数式编程
1+
# 小彭老师带你学函数式编程
22

33
[TOC]
44

@@ -320,12 +320,13 @@ int main() {
320320
- `curl_multi` 提供了超详细的参数,把每个操作分拆成多步,方便用户插手细节,满足高级用户的定制化需求,但太过复杂,难以学习。
321321
- `curl_easy` 是对 `curl_multi` 的再封装,提供了更简单的 API,但是对具体细节就难以操控了,适合初学者上手。
322322

323+
### Linus 的最佳实践:每个函数不要超过 3 层嵌套,一行不要超过 80 字符,每个函数体不要超过 24 行
323324

324-
### Linus 的最佳实践:每个函数不要超过 3 层嵌套,函数体不要超过 24 行
325+
Linux 内核为什么坚持使用 8 缩进为代码风格?
325326

326-
Linux 内核为什么坚持使用 `TAB=8` 为代码风格?
327+
因为高缩进可以避免程序员写出嵌套层数太深的代码,当他写出太深嵌套时,巨大的 8 缩进会让代码变得非常偏右,写不下多少空间。从而让程序员自己红着脸“对不起,我把单个函数写太深了”然后赶紧拆分出多个函数来。
327328

328-
TODO:还在写
329+
此外,他还规定了单一一个函数必须在终端宽度 80 x 24 中显示得下,否则就需要拆分成多个函数重写,这配合 8 缩进,有效的限制了嵌套的层数,迫使程序员不得不重新思考,更解耦的写法出来。
329330

330331
## 为什么需要函数式?
331332

@@ -345,7 +346,7 @@ int sum(std::vector<int> const &v) {
345346

346347
int product(std::vector<int> const &v) {
347348
int ret = v[0];
348-
for (int i = 1; i < v.size(); i++) {
349+
for (int i = 1; i < v.size(); i++) {
349350
ret *= v[i];
350351
}
351352
return ret;
@@ -1295,13 +1296,320 @@ fmt::println("x = {}", x); // 10
12951296
fmt::println("lambda.x = {}", lambda.x); // 编译错误💣编译器产生的匿名 lambda 对象中捕获产生的 x 成员变量是匿名的,无法访问
12961297
```
12971298
1298-
#### `auto` 推导返回值
1299+
## 深入认识 lambda 语法
1300+
1301+
### 捕获列表语法
1302+
1303+
一个变量的三种捕获方式:
1304+
1305+
- 按值拷贝捕获 `[x]`
1306+
- 按值移动捕获 `[x = std::move(x)]`
1307+
- 按引用捕获 `[&x]`
1308+
1309+
TODO
1310+
1311+
批量捕获:
1312+
1313+
- 按值拷贝捕获所有用到的变量 `[=]`
1314+
- 按引用捕获所有用到的变量 `[&]`
1315+
1316+
TODO:与语法糖解构后比较
1317+
1318+
### 类型推导
1319+
1320+
#### `auto` 推导返回类型
1321+
1322+
lambda 函数可以通过在参数列表后使用 `->` 指定函数返回类型:
1323+
1324+
```cpp
1325+
auto lambda = [] (int a) -> int {
1326+
return a;
1327+
};
1328+
int i = lambda();
1329+
```
1330+
1331+
如果不指定返回类型,默认是 `-> auto`,也就是和返回类型声明为 `auto` 一样,会自动根据表达式为你推导返回类型:
1332+
1333+
```cpp
1334+
auto lambda = [] (int a) {
1335+
return a;
1336+
};
1337+
// 等价于:
1338+
auto lambda = [] (int a) -> int {
1339+
return a;
1340+
};
1341+
```
1342+
1343+
```cpp
1344+
auto lambda2 = [] (int a) {
1345+
return a * 2.0; // 此表达式返回 double
1346+
};
1347+
// 等价于:
1348+
auto lambda2 = [] (int a) -> double { // 所以 auto 推导出的返回类型也为 double
1349+
return a * 2.0;
1350+
};
1351+
```
1352+
1353+
#### `auto` 推导参数类型
1354+
1355+
TODO
1356+
1357+
#### `auto` 实现多次实例化的应用
1358+
1359+
#### `auto &``auto const &` 的应用
1360+
1361+
#### `auto &&` 万能引用
1362+
1363+
#### `decltype(auto)` 保留真正的原始返回类型
1364+
1365+
## lambda 常见的三大用法
1366+
1367+
### 储存一个函数对象做局部变量
1368+
1369+
我们总是用 `auto` 来保存一个函数对象作为局部变量,这会自动推导 lambda 的匿名类型。
1370+
1371+
为什么不能显式写出类型名字?因为 lambda 的类型是匿名的,你无法写出类型名,只能通过 `auto` 推导。
1372+
1373+
```cpp
1374+
int b = 2;
1375+
auto lambda = [b] (int a) {
1376+
return a + b;
1377+
};
1378+
```
1379+
1380+
> {{ icon.fun }} 这也是为什么 C++11 同时引入 `auto` 和 lambda 语法的原因。
1381+
1382+
如果你实在需要显式的类名,那就需要使用 `std::function` 容器。虽然 lambda 表达式产生的类型是匿名的,但是该类型符合“可调用”的约束,可以被 `std::function` 容器接纳。
1383+
1384+
> {{ icon.tip }} 即 lambda 类型可隐式转换为相应参数列表的 `std::function` 容器。因为 `std::function<Ret(Args)>` 容器可以接纳任何“可接受 `(Args...)` 参数调用并返回 `Ret` 类型”的任意函数对象。
1385+
1386+
```cpp
1387+
int b = 2;
1388+
std::function<void(int)> lambda = [b] (int a) {
1389+
return a + b;
1390+
};
1391+
```
1392+
1393+
例如当我们需要把 lambda 对象推入 `vector` 等容器中时,就需要显式写出函数对象的类型,此时万能函数对象容器 `std::function` 就能派上用场了:
1394+
1395+
```cpp
1396+
// vector<auto> lambda_list; // 错误:不支持的语法
1397+
vector<function<void(int)>> lambda_list; // OK
1398+
1399+
int b = 2;
1400+
lambda_list.push_back([b] (int a) {
1401+
return a + b;
1402+
};
1403+
lambda_list.push_back([b] (int a) {
1404+
return a * b;
1405+
};
1406+
1407+
for (auto lambda: lambda_list) {
1408+
int ret = lambda(2);
1409+
fmt::println("{}", ret);
1410+
}
1411+
```
1412+
1413+
#### 应用案例
1414+
1415+
##### 代码复用
1416+
1417+
TODO
1418+
1419+
`[&]``[=]`
1420+
1421+
##### 就地调用 lambda-idiom
1422+
1423+
TODO
1424+
1425+
#### 注意捕获变量的生命周期
1426+
1427+
新手用 lambda 常见的错误就是搞不清捕获变量的生命周期,总是想当然地无脑用 `[&]`,非常危险。
1428+
1429+
如果你有“自知之明”,自知不熟悉生命周期分析,那就全部 `[=]`
1430+
1431+
> {{ icon.tip }} 等我们稍后的 [生命周期专题课程](cpp_lifetime.md) 中介绍。
1432+
1433+
实际上,`[=]` 应该是你默认的捕获方式。
1434+
1435+
只有当类型无法拷贝会深拷贝成本过高时,才会选择性地把一些可以改成引用捕获的部分 lambda,使用 `[&]` 来捕获部分需要避免拷贝的变量,或者使用 `shared_ptr` 配合 `[=]` 将深拷贝化为浅拷贝。
1436+
1437+
> {{ icon.fun }} 一些习惯了 Python、JS 等全员 `shared_ptr` 的垃圾回收语言巨婴,一上来就全部无脑 `[&]`,用实际行动证明了智商和勇气成反比定律。
1438+
1439+
好消息是,对于代码复用和就地调用的情况,lambda 对象的生命都不会出函数体,可以安全地改成按引用捕获 `[&]`
1440+
1441+
但是对于下面两种情况(作为参数传入和作为返回值),就不一定有这么幸运了。
1442+
1443+
总之,无论如何要保证 lambda 对象的生命周期 小于等于 按引用捕获的所有变量的生命周期。如果做不到,那就得把这些可能超出的变量改成按值捕获 `[=]`
1444+
1445+
### 返回一个函数对象做返回值
1446+
1447+
如果你想让返回一个函数对象,分为两种情况:
1448+
1449+
就地定义(声明与定义合体)的函数,建议填写 `auto` 为返回值类型,自动推导 lambda 的匿名类型(因为你无法写出具体类型名)。
1450+
1451+
然后,在 `return` 语句中就地写出 lambda 表达式即可:
1452+
1453+
```cpp
1454+
auto make_adder(int x) {
1455+
return [x] (int y) {
1456+
return x + y;
1457+
};
1458+
}
1459+
```
1460+
1461+
分离声明与定义的函数,无法使用 `auto` 推导返回类型,不得不使用万能的函数容器 `std::function` 来擦屁股:
1462+
1463+
```cpp
1464+
// adder.h
1465+
std::function<int()> make_adder(int x);
1466+
1467+
// adder.cpp
1468+
std::function<int()> make_adder(int x) {
1469+
return [x] (int y) {
1470+
return x + y;
1471+
};
1472+
}
1473+
```
1474+
1475+
“函数返回一个函数对象”,这种用法在函数式编程非常常见。
1476+
1477+
#### 应用案例
1478+
1479+
例如上述的 `make_adder` 等于绑定了一个固定参数 `x` 的加法函数,之后每次调用这个返回的函数对象,就固定增加之前在 `make_adder` 参数中 `x` 的增量了。
1480+
1481+
TODO
1482+
1483+
#### 注意捕获变量的生命周期
1484+
1485+
此类“返回一个函数对象”的写法,其 lambda 捕获必须是按值捕获的!
1486+
1487+
否则,因为调用者调用返回的函数对象时,局部变量和实参所对应的函数局部栈空间已经释放,相当于在 lambda 体内存有空悬引用,导致出现未定义行为(要么直接崩溃,要么非常隐蔽地留下内存非法访问的隐患)。
1488+
1489+
```cpp
1490+
auto make_adder(int x) {
1491+
return [x] (int y) {
1492+
return x + y;
1493+
};
1494+
}
1495+
1496+
int main() { // 我是调用者
1497+
auto adder = make_adder(2);
1498+
adder(3); // 2 + 3 = 5
1499+
}
1500+
```
1501+
1502+
### 接受一个函数对象做参数
1503+
1504+
TODO:代码
1505+
1506+
#### 应用案例
1507+
1508+
TODO:策略模式
1509+
1510+
TODO:延迟回调
1511+
1512+
#### 注意捕获变量的生命周期
1513+
1514+
函数对象做参数的生命周期问题,需要分就地调用和延迟调用两种情况讨论。
1515+
1516+
### 生命周期问题总结:何时使用 `[=]` 或 `[&]`
1517+
1518+
如果你的智力暂不足以搞懂生命周期分析,没关系,始终使用 `[=]` 肯定没错。
1519+
1520+
> {{ icon.tip }} 一个同学询问:我口渴!在不知道他的耐受度的情况下,我肯定是直接给他吃水,而不是给他吃酒精。虽然一些孝子曰“适量”“适度”“计量”各种一连串附加条件下,宣称“酒精也是安全的”。但是“水永远是安全的”,“永远”,那我直接给他喝水,是肯定不会错的。等你长大成年了,有辨别能力了,再去根据自己的小计机瘙痒程度,选择性地喝有机溶剂。此处 `[=]` 就是这个万能的水,虽然不一定高效,但是肯定没错。初学者总是从 `[=]` 用起,等学明白了,再来尝试突破“小计机性能焦虑优化”也不迟。
1521+
1522+
如果你自认为能分得清:
1523+
1524+
- 在当前函数体内创建,当前函数体内立即调用,可以引用捕获 `[&]`,但值捕获 `[=]` 也没错。
1525+
- 返回一个 lambda,必须值捕获 `[=]`。
1526+
- 接受一个 lambda 做参数,需要进一步分为两种情况:
1527+
- 在当前函数体内立即调用,可以引用捕获 `[&]`,但值捕获 `[=]` 也没错。
1528+
- 作为回调函数,延迟调用,那就必须值捕获 `[=]`。
1529+
1530+
以上四种情况,分别代码演示:
1531+
1532+
```cpp
1533+
void func() {
1534+
int i = 1;
1535+
auto lambda = [&] () { return i; };
1536+
lambda();
1537+
}
1538+
1539+
int main() {
1540+
func();
1541+
}
1542+
```
1543+
1544+
```cpp
1545+
auto func() {
1546+
int i = 1;
1547+
return [=] () { return i; };
1548+
}
1549+
1550+
int main() {
1551+
auto lambda = func();
1552+
lambda();
1553+
}
1554+
```
1555+
1556+
```cpp
1557+
auto func(auto lambda) {
1558+
lambda();
1559+
}
1560+
1561+
int main() {
1562+
int i = 1;
1563+
func([&] () { return i; });
1564+
}
1565+
```
1566+
1567+
```cpp
1568+
vector<function<int()>> g_callbacks;
1569+
auto func(auto lambda) {
1570+
g_callbacks.push_back(lambda);
1571+
}
1572+
1573+
void init() {
1574+
int i = 1;
1575+
func([=] () { return i; });
1576+
}
1577+
1578+
int main() {
1579+
init();
1580+
for (auto cb: g_callbacks) {
1581+
cb();
1582+
}
1583+
}
1584+
```
1585+
1586+
## lambda 进阶案例
1587+
1588+
### lambda 实现递归
1589+
1590+
### lambda 避免全局重载函数捕获为变量时恼人的错误
1591+
1592+
### lambda 配合 if-constexpr 实现编译期三目运算符
1593+
1594+
### 推荐用 C++23 的 `std::move_only_function` 取代 `std::function`
1595+
1596+
通过按值移动捕获,lambda 可以持有一个 unique_ptr 作为捕获变量。
1597+
1598+
TODO
1599+
1600+
### 用于类模板参数的仿函数时,需与 `decltype` 的配合
1601+
1602+
### 无状态 lambda 隐式转换为函数指针
1603+
1604+
### `std::variant` 配合实现动态多态
1605+
1606+
TODO
12991607

1300-
#### `auto` 推导参数
1608+
在之后的 `std::variant` 专题章节中会进一步介绍。
13011609

1302-
#### 与 `decltype` 的配合
1610+
### 配合 `shared_from_this` 实现延长 this 生命周期
13031611

1304-
#### 无状态 lambda 隐式转换为函数指针
1612+
### `mutable` lambda 实现计数器
13051613

13061614
## bind 为函数对象绑定参数
13071615

docs/threading.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# C++ 多线程编程(未完工)
22

3+
## 创建线程
4+
5+
TODO
6+
37
## 为什么数据竞争
48

59
```cpp

0 commit comments

Comments
 (0)