@@ -2358,12 +2358,186 @@ TODO:`std::less` 和 `std::bind`
23582358
23592359### 函数指针是 C 语言陋习,改掉
23602360
2361+ 无法保存状态
2362+
23612363## lambda 进阶案例
23622364
23632365### lambda 实现递归
23642366
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+
23652532### lambda 避免全局重载函数捕获为变量时恼人的错误
23662533
2534+ ```cpp
2535+ void print(int i);
2536+ void print(std::string s);
2537+
2538+ auto f = print; // 出错!无法确定是哪一个重载!
2539+ ```
2540+
23672541### lambda 配合 if-constexpr 实现编译期三目运算符
23682542
23692543### 推荐用 C++23 的 ` std::move_only_function ` 取代 ` std::function `
0 commit comments