@@ -385,7 +385,14 @@ <h1 id="_1">小彭老师带你学函数式编程</h1>
385385</ ul >
386386</ li >
387387< li > < a href ="#lambda_3 "> lambda 进阶案例</ a > < ul >
388- < li > < a href ="#lambda_4 "> lambda 实现递归</ a > </ li >
388+ < li > < a href ="#lambda_4 "> lambda 实现递归</ a > < ul >
389+ < li > < a href ="#_42 "> 初始化定义可以包含自己?!</ a > </ li >
390+ < li > < a href ="#auto_4 "> auto 才是罪魁祸首</ a > </ li >
391+ < li > < a href ="#_43 "> 写明类型</ a > </ li >
392+ < li > < a href ="#_44 "> 性能焦虑!</ a > </ li >
393+ < li > < a href ="#c23 "> 没有 C++23?</ a > </ li >
394+ </ ul >
395+ </ li >
389396< li > < a href ="#lambda_5 "> lambda 避免全局重载函数捕获为变量时恼人的错误</ a > </ li >
390397< li > < a href ="#lambda-if-constexpr "> lambda 配合 if-constexpr 实现编译期三目运算符</ a > </ li >
391398< li > < a href ="#c23-stdmove_only_function-stdfunction "> 推荐用 C++23 的 std::move_only_function 取代 std::function</ a > </ li >
@@ -2354,9 +2361,132 @@ <h4 id="lambda_2">使用 lambda 代替</h4>
23542361< h3 id ="bind_3 "> bind 与标准库自带的运算符仿函数配合</ h3 >
23552362< p > TODO:< code > std::less</ code > 和 < code > std::bind</ code > </ p >
23562363< h3 id ="c "> 函数指针是 C 语言陋习,改掉</ h3 >
2364+ < p > 无法保存状态</ p >
23572365< h2 id ="lambda_3 "> lambda 进阶案例</ h2 >
23582366< h3 id ="lambda_4 "> lambda 实现递归</ h3 >
2367+ < pre > < code class ="language-cpp "> int fib(int n) {
2368+ if (n <= 2) {
2369+ return 1;
2370+ }
2371+ return fib(n - 1) + fib(n - 2);
2372+ }
2373+ </ code > </ pre >
2374+ < p > 以上代码是众所周知的,典中典之斐波那契数列第 n 项的递归求法。</ p >
2375+ < p > 然而这需要定义一个全局函数 fib,污染了全局名字空间不说,还无法捕获到局部变量。</ p >
2376+ < p > 有时你可能希望在局部定义一个递归函数,就适合用 Lambda 语法,在一个现有的函数体内就地创建 Lambda 函数对象,而无需污染全局。</ p >
2377+ < pre > < code class ="language-cpp "> int main() {
2378+ auto fib = [&] (int n) {
2379+ if (n <= 2) {
2380+ return 1;
2381+ }
2382+ return fib(n - 1) + fib(n - 2);
2383+ };
2384+ }
2385+ </ code > </ pre >
2386+ < p > 然而以上代码会编译出错!因为 < code > fib</ code > 的< strong > 初始化定义</ strong > 用的表达式 < code > [&] (int n) { return fib(); }</ code > 用到了 < code > fib</ code > 自己!</ p >
2387+ < h4 id ="_42 "> 初始化定义可以包含自己?!</ h4 >
2388+ < p > 那 C++ 中,什么情况下一个变量的< strong > 初始化定义</ strong > 可以包含自己呢?让我们回顾一下:</ p >
2389+ < pre > < code class ="language-cpp "> int i = i + 1;
2390+ </ code > </ pre >
2391+ < p > 虽然编译能够通过,显然会产生运行时未定义行为。</ p >
2392+ < p > 因为在执行变量 < code > i</ code > 的初始化定义表达式 < code > i + 1</ code > 时,< code > i</ code > 还没有初始化呢!读取未初始化的变量是未定义行为。</ p >
2393+ < pre > < code class ="language-cpp "> int i = (int) &i;
2394+ </ code > </ pre >
2395+ < p > 可以编译通过(假设为 32 位环境)。</ p >
2396+ < p > 则是允许的,因为虽然 < code > i</ code > 的初始化表达式 < code > (int) &i</ code > 包含了尚未初始化的自己 < code > i</ code > ,但却是以他的地址形态出现的(使用了取地址运算符 < code > &</ code > )。</ p >
2397+ < p > 也就是说我们初始化 < code > i</ code > 只是用到了 < code > i</ code > 变量的地址 < code > &i</ code > ,而不是用到 < code > i</ code > 里面的值。</ p >
2398+ < p > 用变量自己的地址,初始化自己的值,没有问题。</ p >
2399+ < p > 因为一个变量的生命周期中,总是先确定了其地址,再初始化其中的值的;无论是 new 还是局部变量,都是先有地址再初始化其值。</ p >
2400+ < blockquote >
2401+ < p > < img src ="../img/bulb.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 顺序:分配地址 -> 初始化值</ p >
2402+ </ blockquote >
2403+ < p > 所以我们初始化 < code > fib</ code > 的表达式中,用 < code > [&]</ code > 捕获了 < code > fib</ code > 自己的引用(变量的地址),是没问题的。</ p >
2404+ < h4 id ="auto_4 "> < code > auto</ code > 才是罪魁祸首</ h4 >
2405+ < p > 真正导致无法编译的问题在于:我们使用 < code > auto</ code > 来推导 < code > fib</ code > 的类型,而 < code > auto</ code > 变量的类型,取决于右侧表达式的类型,必须先知道右侧表达式的类型,才能知道变量是什么类型,才能为变量分配地址,然后赋初始值。</ p >
2406+ < blockquote >
2407+ < p > < img src ="../img/bulb.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 顺序:确定类型 -> 分配地址 -> 初始化值</ p >
2408+ </ blockquote >
2409+ < p > 分配地址需要用到类型信息,而 < code > auto</ code > 变量的类型信息取决于右侧表达式的类型。</ p >
2410+ < p > 要知道右侧表达式的类型,就需要右侧表达式完成编译。</ p >
2411+ < p > 而右侧表达式中包含了 < code > fib</ code > 变量自己的引用捕获 < code > [&]</ code > 。</ p >
2412+ < p > 这导致 < code > fib</ code > 的类型还没有确定时,就需要被捕获进 Lambda 了,这就出现了循环引用,编译不通过。</ p >
2413+ < blockquote >
2414+ < p > < img src ="../img/awesomeface.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 一场由 < code > auto</ code > 推导机制引发的血案。</ p >
2415+ </ blockquote >
2416+ < h4 id ="_43 "> 写明类型</ h4 >
2417+ < p > 要避免这种循环引用,我们只能避免使用 < code > auto</ code > ,在 < code > fib</ code > 定义中,就写一个具体的类型。</ p >
2418+ < pre > < code class ="language-cpp "> int main() {
2419+ std::function<int(int)> fib = [&] (int n) {
2420+ if (n <= 2) {
2421+ return 1;
2422+ }
2423+ return fib(n - 1) + fib(n - 2);
2424+ };
2425+ }
2426+ </ code > </ pre >
2427+ < p > < code > function</ code > 类型的大小,在 < code > fib</ code > 初始化之前就已经确定,与 < code > fib</ code > 初始化为什么值无关。</ p >
2428+ < p > 这样在编译 < code > fib</ code > 的初始化表达式时,< code > fib</ code > 就是已经确定类型,并分配好内存地址了的,就可以被他自己的初始化表达式中的 Lambda 捕获。</ p >
2429+ < h4 id ="_44 "> 性能焦虑!</ h4 >
2430+ < p > 但是有的同学说,< code > function</ code > 是类型擦除容器,虽然很方便,但是低性能呀?我有性能焦虑症😩,能不能还用 < code > auto</ code > 呀?</ p >
2431+ < p > 的确,因为 Lambda 表达式本身的类型是一个匿名类型,并不是 < code > function<int(int)></ code > 类型,这之间发生了隐式转换。</ p >
2432+ < p > 为了伺候你的性能焦虑😩,小彭老师隆重介绍一种能让 Lambda 递归的 C++23 语法 deducing-this:</ p >
2433+ < pre > < code class ="language-cpp "> auto fib = [] (this auto &self, int n) {
2434+ if (n <= 2) {
2435+ return 1;
2436+ }
2437+ return self(n - 1) + self(n - 2);
2438+ };
2439+ </ code > </ pre >
2440+ < p > 且无需用 < code > [&]</ code > 捕获 < code > fib</ code > 自己,用 < code > self</ code > 这个特殊的参数就能访问到自身的引用!</ p >
2441+ < p > 之前也说了,Lambda 无非是编译器自动帮你生成了一个带有 < code > operator()</ code > 成员函数的匿名类,他实际上等价于:</ p >
2442+ < pre > < code class ="language-cpp "> struct Fib {
2443+ int operator()(int n) const {
2444+ if (n <= 2) {
2445+ return 1;
2446+ }
2447+ return (*this)(n - 1) + (*this)(n - 2); // deducing-this 定义的 self 引用等价于 *this
2448+ };
2449+ };
2450+ auto fib = Fib();
2451+ </ code > </ pre >
2452+ < p > 毕竟 < code > this</ code > 是调用 < code > Fib::operator()</ code > 时本来就会传入的参数,根本没必要储存在 < code > Fib</ code > 类型体内,更节省了内存。</ p >
2453+ < p > 只是由于 C++23 之前在 Lambda 体内写 this,含义是外部类的指针,而不是 Lambda 对象自己的 this 指针。</ p >
2454+ < p > 所以 C++23 才提出了 deducing-this,把本就属于 Fib 的 this 作为参数传入,获取 Lambda 自己的地址。</ p >
2455+ < blockquote >
2456+ < p > < img src ="../img/bulb.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> deducing-this 的语法固定为 < code > this auto</ code > ,这里的 < code > auto</ code > 会自动推导为当前 Lambda 对象的类型(是个匿名类)。而前缀 < code > this</ code > 是固定的语法,无特殊含义。</ p >
2457+ </ blockquote >
2458+ < h4 id ="c23 "> 没有 C++23?</ h4 >
2459+ < p > 如果你无法使用 C++23,还患有性能焦虑,不想用 function,还有一种小技巧可以让 Lambda 支持递归:在参数中传入自身的引用!</ p >
2460+ < pre > < code class ="language-cpp "> auto fib = [] (auto &fib, int n) -> int {
2461+ if (n <= 2) {
2462+ return 1;
2463+ }
2464+ return fib(fib, n - 1) + fib(fib, n - 2);
2465+ };
2466+ </ code > </ pre >
2467+ < blockquote >
2468+ < p > < img src ="../img/question.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 这在函数式编程范式中称为“自递归”技巧,可以让无法一个捕获到自身的匿名函数对象也能实现递归自我调用。</ p >
2469+ </ blockquote >
2470+ < p > 缺点:</ p >
2471+ < ol >
2472+ < li > 每次使用时就需要把 < code > fib</ code > 作为引用参数传入用自己!</ li >
2473+ </ ol >
2474+ < pre > < code class ="language-cpp "> fib(fib, 1);
2475+ </ code > </ pre >
2476+ < ol start ="2 ">
2477+ < li > 必须写明返回类型 < code > -> int</ code > ,否则编译会失败!</ li >
2478+ </ ol >
2479+ < p > 因为 C++ 编译器递归解析表达式的设计,需要先确定 Lambda 表达式中每一条子语句————例如 < code > fib(fib, n - 1)</ code > ————的返回类型,才能确定 Lambda 自身的返回类型。</ p >
2480+ < p > 而 < code > fib(fib, n - 1)</ code > 这个表达式又需要用到 < code > fib</ code > 的类型,其又进一步需要 < code > fib</ code > 自身体内每一条子语句,也就是 < code > fib(fib, n - 1)</ code > 的类型,无限递归,无法确定唯一的类型,编译器只能报错。</ p >
2481+ < blockquote >
2482+ < p > < img src ="../img/warning.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 使用这种“自递归”技巧的 Lambda,哪怕没有返回值,也必须写明 < code > -> void</ code > !非常麻烦……</ p >
2483+ </ blockquote >
23592484< h3 id ="lambda_5 "> lambda 避免全局重载函数捕获为变量时恼人的错误</ h3 >
2485+ < pre > < code class ="language-cpp "> void print(int i);
2486+ void print(std::string s);
2487+
2488+ auto f = print; // 出错!无法确定是哪一个重载!
2489+ </ code > </ pre >
23602490< h3 id ="lambda-if-constexpr "> lambda 配合 if-constexpr 实现编译期三目运算符</ h3 >
23612491< h3 id ="c23-stdmove_only_function-stdfunction "> 推荐用 C++23 的 < code > std::move_only_function</ code > 取代 < code > std::function</ code > </ h3 >
23622492< p > 通过按值移动捕获 < code > [p = std::move(p)]</ code > ,lambda 可以持有一个 unique_ptr 作为捕获变量。</ p >
0 commit comments