1
- # 函数式编程
1
+ # 小彭老师带你学函数式编程
2
2
3
3
[ TOC]
4
4
@@ -320,12 +320,13 @@ int main() {
320
320
- `curl_multi` 提供了超详细的参数,把每个操作分拆成多步,方便用户插手细节,满足高级用户的定制化需求,但太过复杂,难以学习。
321
321
- `curl_easy` 是对 `curl_multi` 的再封装,提供了更简单的 API,但是对具体细节就难以操控了,适合初学者上手。
322
322
323
+ ### Linus 的最佳实践:每个函数不要超过 3 层嵌套,一行不要超过 80 字符,每个函数体不要超过 24 行
323
324
324
- ### Linus 的最佳实践:每个函数不要超过 3 层嵌套,函数体不要超过 24 行
325
+ Linux 内核为什么坚持使用 8 缩进为代码风格?
325
326
326
- Linux 内核为什么坚持使用 ` TAB=8 ` 为代码风格?
327
+ 因为高缩进可以避免程序员写出嵌套层数太深的代码,当他写出太深嵌套时,巨大的 8 缩进会让代码变得非常偏右,写不下多少空间。从而让程序员自己红着脸“对不起,我把单个函数写太深了”然后赶紧拆分出多个函数来。
327
328
328
- TODO:还在写
329
+ 此外,他还规定了单一一个函数必须在终端宽度 80 x 24 中显示得下,否则就需要拆分成多个函数重写,这配合 8 缩进,有效的限制了嵌套的层数,迫使程序员不得不重新思考,更解耦的写法出来。
329
330
330
331
## 为什么需要函数式?
331
332
@@ -345,7 +346,7 @@ int sum(std::vector<int> const &v) {
345
346
346
347
int product(std::vector<int > const &v) {
347
348
int ret = v[ 0] ;
348
- for (int i = 1; i < v.size(); i++) {
349
+ for (int i = 1; i < v.size(); i++) {
349
350
ret * = v[ i] ;
350
351
}
351
352
return ret;
@@ -1295,13 +1296,320 @@ fmt::println("x = {}", x); // 10
1295
1296
fmt::println("lambda.x = {}", lambda.x); // 编译错误💣编译器产生的匿名 lambda 对象中捕获产生的 x 成员变量是匿名的,无法访问
1296
1297
```
1297
1298
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
1299
1607
1300
- #### `auto` 推导参数
1608
+ 在之后的 ` std::variant ` 专题章节中会进一步介绍。
1301
1609
1302
- #### 与 `decltype` 的配合
1610
+ ### 配合 ` shared_from_this ` 实现延长 this 生命周期
1303
1611
1304
- #### 无状态 lambda 隐式转换为函数指针
1612
+ ### ` mutable ` lambda 实现计数器
1305
1613
1306
1614
## bind 为函数对象绑定参数
1307
1615
0 commit comments