@@ -2358,12 +2358,186 @@ TODO:`std::less` 和 `std::bind`
2358
2358
2359
2359
### 函数指针是 C 语言陋习,改掉
2360
2360
2361
+ 无法保存状态
2362
+
2361
2363
## lambda 进阶案例
2362
2364
2363
2365
### lambda 实现递归
2364
2366
2367
+ ``` cpp
2368
+ int fib (int n) {
2369
+ if (n <= 2) {
2370
+ return 1;
2371
+ }
2372
+ return fib(n - 1) + fib(n - 2);
2373
+ }
2374
+ ```
2375
+
2376
+ 以上代码是众所周知的,典中典之斐波那契数列第 n 项的递归求法。
2377
+
2378
+ 然而这需要定义一个全局函数 fib,污染了全局名字空间不说,还无法捕获到局部变量。
2379
+
2380
+ 有时你可能希望在局部定义一个递归函数,就适合用 Lambda 语法,在一个现有的函数体内就地创建 Lambda 函数对象,而无需污染全局。
2381
+
2382
+ ```cpp
2383
+ int main() {
2384
+ auto fib = [&] (int n) {
2385
+ if (n <= 2) {
2386
+ return 1;
2387
+ }
2388
+ return fib(n - 1) + fib(n - 2);
2389
+ };
2390
+ }
2391
+ ```
2392
+
2393
+ 然而以上代码会编译出错!因为 ` fib ` 的** 初始化定义** 用的表达式 ` [&] (int n) { return fib(); } ` 用到了 ` fib ` 自己!
2394
+
2395
+ #### 初始化定义可以包含自己?!
2396
+
2397
+ 那 C++ 中,什么情况下一个变量的** 初始化定义** 可以包含自己呢?让我们回顾一下:
2398
+
2399
+ ``` cpp
2400
+ int i = i + 1 ;
2401
+ ```
2402
+
2403
+ 虽然编译能够通过,显然会产生运行时未定义行为。
2404
+
2405
+ 因为在执行变量 ` i ` 的初始化定义表达式 ` i + 1 ` 时,` i ` 还没有初始化呢!读取未初始化的变量是未定义行为。
2406
+
2407
+ ``` cpp
2408
+ int i = (int ) &i;
2409
+ ```
2410
+
2411
+ 可以编译通过(假设为 32 位环境)。
2412
+
2413
+ 则是允许的,因为虽然 ` i ` 的初始化表达式 ` (int) &i ` 包含了尚未初始化的自己 ` i ` ,但却是以他的地址形态出现的(使用了取地址运算符 ` & ` )。
2414
+
2415
+ 也就是说我们初始化 ` i ` 只是用到了 ` i ` 变量的地址 ` &i ` ,而不是用到 ` i ` 里面的值。
2416
+
2417
+ 用变量自己的地址,初始化自己的值,没有问题。
2418
+
2419
+ 因为一个变量的生命周期中,总是先确定了其地址,再初始化其中的值的;无论是 new 还是局部变量,都是先有地址再初始化其值。
2420
+
2421
+ > {{ icon.tip }} 顺序:分配地址 -> 初始化值
2422
+
2423
+ 所以我们初始化 ` fib ` 的表达式中,用 ` [&] ` 捕获了 ` fib ` 自己的引用(变量的地址),是没问题的。
2424
+
2425
+ #### ` auto ` 才是罪魁祸首
2426
+
2427
+ 真正导致无法编译的问题在于:我们使用 ` auto ` 来推导 ` fib ` 的类型,而 ` auto ` 变量的类型,取决于右侧表达式的类型,必须先知道右侧表达式的类型,才能知道变量是什么类型,才能为变量分配地址,然后赋初始值。
2428
+
2429
+ > {{ icon.tip }} 顺序:确定类型 -> 分配地址 -> 初始化值
2430
+
2431
+ 分配地址需要用到类型信息,而 ` auto ` 变量的类型信息取决于右侧表达式的类型。
2432
+
2433
+ 要知道右侧表达式的类型,就需要右侧表达式完成编译。
2434
+
2435
+ 而右侧表达式中包含了 ` fib ` 变量自己的引用捕获 ` [&] ` 。
2436
+
2437
+ 这导致 ` fib ` 的类型还没有确定时,就需要被捕获进 Lambda 了,这就出现了循环引用,编译不通过。
2438
+
2439
+ > {{ icon.fun }} 一场由 ` auto ` 推导机制引发的血案。
2440
+
2441
+ #### 写明类型
2442
+
2443
+ 要避免这种循环引用,我们只能避免使用 ` auto ` ,在 ` fib ` 定义中,就写一个具体的类型。
2444
+
2445
+ ``` cpp
2446
+ int main () {
2447
+ std::function<int(int)> fib = [&] (int n) {
2448
+ if (n <= 2) {
2449
+ return 1;
2450
+ }
2451
+ return fib(n - 1) + fib(n - 2);
2452
+ };
2453
+ }
2454
+ ```
2455
+
2456
+ ` function ` 类型的大小,在 ` fib ` 初始化之前就已经确定,与 ` fib ` 初始化为什么值无关。
2457
+
2458
+ 这样在编译 ` fib ` 的初始化表达式时,` fib ` 就是已经确定类型,并分配好内存地址了的,就可以被他自己的初始化表达式中的 Lambda 捕获。
2459
+
2460
+ #### 性能焦虑!
2461
+
2462
+ 但是有的同学说,` function ` 是类型擦除容器,虽然很方便,但是低性能呀?我有性能焦虑症😩,能不能还用 ` auto ` 呀?
2463
+
2464
+ 的确,因为 Lambda 表达式本身的类型是一个匿名类型,并不是 ` function<int(int)> ` 类型,这之间发生了隐式转换。
2465
+
2466
+ 为了伺候你的性能焦虑😩,小彭老师隆重介绍一种能让 Lambda 递归的 C++23 语法 deducing-this:
2467
+
2468
+ ``` cpp
2469
+ auto fib = [] (this auto &self, int n) {
2470
+ if (n <= 2) {
2471
+ return 1;
2472
+ }
2473
+ return self(n - 1) + self(n - 2);
2474
+ };
2475
+ ```
2476
+
2477
+ 且无需用 ` [&] ` 捕获 ` fib ` 自己,用 ` self ` 这个特殊的参数就能访问到自身的引用!
2478
+
2479
+ 之前也说了,Lambda 无非是编译器自动帮你生成了一个带有 ` operator() ` 成员函数的匿名类,他实际上等价于:
2480
+
2481
+ ``` cpp
2482
+ struct Fib {
2483
+ int operator()(int n) const {
2484
+ if (n <= 2) {
2485
+ return 1;
2486
+ }
2487
+ return (* this)(n - 1) + (* this)(n - 2); // deducing-this 定义的 self 引用等价于 * this
2488
+ };
2489
+ };
2490
+ auto fib = Fib();
2491
+ ```
2492
+
2493
+ 毕竟 `this` 是调用 `Fib::operator()` 时本来就会传入的参数,根本没必要储存在 `Fib` 类型体内,更节省了内存。
2494
+
2495
+ 只是由于 C++23 之前在 Lambda 体内写 this,含义是外部类的指针,而不是 Lambda 对象自己的 this 指针。
2496
+
2497
+ 所以 C++23 才提出了 deducing-this,把本就属于 Fib 的 this 作为参数传入,获取 Lambda 自己的地址。
2498
+
2499
+ > {{ icon.tip }} deducing-this 的语法固定为 `this auto`,这里的 `auto` 会自动推导为当前 Lambda 对象的类型(是个匿名类)。而前缀 `this` 是固定的语法,无特殊含义。
2500
+
2501
+ #### 没有 C++23?
2502
+
2503
+ 如果你无法使用 C++23,还患有性能焦虑,不想用 function,还有一种小技巧可以让 Lambda 支持递归:在参数中传入自身的引用!
2504
+
2505
+ ```cpp
2506
+ auto fib = [] (auto &fib, int n) -> int {
2507
+ if (n <= 2) {
2508
+ return 1;
2509
+ }
2510
+ return fib(fib, n - 1) + fib(fib, n - 2);
2511
+ };
2512
+ ```
2513
+
2514
+ > {{ icon.detail }} 这在函数式编程范式中称为“自递归”技巧,可以让无法一个捕获到自身的匿名函数对象也能实现递归自我调用。
2515
+
2516
+ 缺点:
2517
+
2518
+ 1 . 每次使用时就需要把 ` fib ` 作为引用参数传入用自己!
2519
+
2520
+ ``` cpp
2521
+ fib (fib, 1);
2522
+ ```
2523
+
2524
+ 2. 必须写明返回类型 `-> int`,否则编译会失败!
2525
+
2526
+ 因为 C++ 编译器递归解析表达式的设计,需要先确定 Lambda 表达式中每一条子语句————例如 `fib(fib, n - 1)`————的返回类型,才能确定 Lambda 自身的返回类型。
2527
+
2528
+ 而 `fib(fib, n - 1)` 这个表达式又需要用到 `fib` 的类型,其又进一步需要 `fib` 自身体内每一条子语句,也就是 `fib(fib, n - 1)` 的类型,无限递归,无法确定唯一的类型,编译器只能报错。
2529
+
2530
+ > {{ icon.warn }} 使用这种“自递归”技巧的 Lambda,哪怕没有返回值,也必须写明 `-> void`!非常麻烦……
2531
+
2365
2532
### lambda 避免全局重载函数捕获为变量时恼人的错误
2366
2533
2534
+ ```cpp
2535
+ void print(int i);
2536
+ void print(std::string s);
2537
+
2538
+ auto f = print; // 出错!无法确定是哪一个重载!
2539
+ ```
2540
+
2367
2541
### lambda 配合 if-constexpr 实现编译期三目运算符
2368
2542
2369
2543
### 推荐用 C++23 的 ` std::move_only_function ` 取代 ` std::function `
0 commit comments