Skip to content

Commit c8e28b5

Browse files
committed
update lambda abt auto
1 parent 1d91dc1 commit c8e28b5

File tree

3 files changed

+227
-52
lines changed

3 files changed

+227
-52
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.12)
22

3-
set(CMAKE_CXX_STANDARD 23)
3+
set(CMAKE_CXX_STANDARD 20)
44

55
project(main)
66

docs/lambda.md

+174
Original file line numberDiff line numberDiff line change
@@ -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`

examples/error_code.cpp

+52-51
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <fmt/core.h>
22
#include <system_error>
3+
#include <span>
34
#include "tl-expected.hpp"
45
#include <fcntl.h>
56
// 今日主题:现代 C++ 中的错误处理
@@ -139,54 +140,54 @@ int main() {
139140
return 0;
140141
}
141142

142-
namespace screenshot1 {
143-
144-
namespace std {
145-
using namespace ::tl;
146-
using namespace ::std;
147-
}
148-
149-
std::expected<int, std::error_code> expectedStdError(int ret) {
150-
if (ret == -1) {
151-
return std::unexpected{std::error_code(errno, std::generic_category())};
152-
}
153-
return ret;
154-
}
155-
156-
struct File {
157-
int fd;
158-
159-
explicit File(const char *path, int flags) {
160-
fd = expectedStdError(::open(path, flags)).value();
161-
}
162-
163-
tl::expected<size_t, std::error_code> write(std::span<const char> buf) {
164-
return expectedStdError(::write(fd, buf.data(), buf.size()));
165-
}
166-
};
167-
168-
std::expected<int, std::error_code> sqrt(int x) {
169-
if (x < 0)
170-
return std::unexpected{make_error_code(std::errc::argument_out_of_domain)};
171-
172-
for (int i = 0;; i++)
173-
if (i * i >= x)
174-
return i;
175-
}
176-
177-
}
178-
179-
namespace screenshot2 {
180-
181-
int sqrt(int x) {
182-
if (x < 0) {
183-
errno = EDOM;
184-
return -1;
185-
}
186-
187-
for (int i = 0;; i++)
188-
if (i * i >= x)
189-
return i;
190-
}
191-
192-
}
143+
// namespace screenshot1 {
144+
//
145+
// namespace std {
146+
// using namespace ::tl;
147+
// using namespace ::std;
148+
// }
149+
//
150+
// std::expected<int, std::error_code> expectedStdError(int ret) {
151+
// if (ret == -1) {
152+
// return std::unexpected{std::error_code(errno, std::generic_category())};
153+
// }
154+
// return ret;
155+
// }
156+
//
157+
// struct File {
158+
// int fd;
159+
//
160+
// explicit File(const char *path, int flags) {
161+
// fd = expectedStdError(::open(path, flags)).value();
162+
// }
163+
//
164+
// tl::expected<size_t, std::error_code> write(std::span<const char> buf) {
165+
// return expectedStdError(::write(fd, buf.data(), buf.size()));
166+
// }
167+
// };
168+
//
169+
// std::expected<int, std::error_code> sqrt(int x) {
170+
// if (x < 0)
171+
// return std::unexpected{make_error_code(std::errc::argument_out_of_domain)};
172+
//
173+
// for (int i = 0;; i++)
174+
// if (i * i >= x)
175+
// return i;
176+
// }
177+
//
178+
// }
179+
//
180+
// namespace screenshot2 {
181+
//
182+
// int sqrt(int x) {
183+
// if (x < 0) {
184+
// errno = EDOM;
185+
// return -1;
186+
// }
187+
//
188+
// for (int i = 0;; i++)
189+
// if (i * i >= x)
190+
// return i;
191+
// }
192+
//
193+
// }

0 commit comments

Comments
 (0)