Skip to content

Commit a6b0691

Browse files
committed
Merge branch 'main' of github.com:parallel101/cppguidebook
2 parents 03495da + 365fe3e commit a6b0691

File tree

7 files changed

+329
-104
lines changed

7 files changed

+329
-104
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/cpp_tricks.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ void calc_average() {
625625
例如字符串切片函数典型的一种实现中,因为“尾巴”的伺候和“主体”的伺候,就会产生重复代码:
626626

627627
```cpp
628-
vector<string> spilt(string str) {
628+
vector<string> split(string str) {
629629
vector<string> list;
630630
string last;
631631
for (char c: str) {
@@ -644,7 +644,7 @@ vector<string> spilt(string str) {
644644
上面的代码中重复的部分 `list.push_back(last);` 可以用 Lambda 复用,把重复的操作封装成局部的 Lambda:
645645
646646
```cpp
647-
vector<string> spilt(string str) {
647+
vector<string> split(string str) {
648648
vector<string> list;
649649
string last;
650650
auto push_last = [&] {

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`

docs/platform.md

+68-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
[TOC]
44

5-
TODO
6-
75
## IDE 不是编译器!
86

97
TODO
@@ -187,7 +185,69 @@ MSVC:`/Dmacro=value`
187185
188186
### 标准库的调试模式
189187

190-
TODO
188+
不少标准库都有提供
189+
190+
#### GCC 开启标准库调试模式
191+
192+
如果你用的是 GCC 默认的 libstdc++ 库,可以定义宏 `#define _GLIBCXX_DEBUG` 以开启调试模式,此模式下会帮助检测标准库用法上出现的未定义行为,例如尝试解引用 `end()` 迭代器时,会抛出错误信息,帮助你检查代码中有没有未定义行为。
193+
194+
也可以通过命令行选项 `-D_GLIBCXX_DEBUG` 开启。
195+
196+
在 CMake 中添加以下选项,即可对特定目标程序启用标准库调试模式:
197+
198+
```cmake
199+
target_compile_definitions(你的程序 PRIVATE _GLIBCXX_DEBUG)
200+
```
201+
202+
> {{ icon.fun }} 有的人会老老实实的复制粘贴,连“你的程序”都忘记改成你真正的程序名。
203+
204+
在 CMakeLists.txt 最前面加上可以对所有接下来的目标启用:
205+
206+
```cmake
207+
add_compile_definitions(_GLIBCXX_DEBUG)
208+
209+
add_executable(你的程序1) # 示例
210+
add_executable(你的程序2) # 示例
211+
```
212+
213+
注意:
214+
215+
开启此选项后,如果你的 exe 程序链接了其他用 C++ 写的库,那么这些库也必须开启调试模式!否则 ABI 会出现不兼容,导致奔溃。这是因为 `_GLIBCXX_DEBUG` 开启后,会改变 C++ 容器的布局和大小(为了调试而塞入的额外帮手变量导致容器结构与开启前不同),链接其他用 C 语言写的库不会有问题,因为此宏仅会影响标准库中 C++ 的部分。
216+
217+
开启标准库调试模式后,因为需要做很多参数安全性检查,性能会受损,建议仅在调试环节启用!等你调试完消除了未定义行为后,在最终发布的版本中中关闭,恢复正常性能。
218+
219+
你可以通过判定 `CMAKE_BUILD_TYPE` 是否等于 `Debug`,实现只在 Debug 模式下启用标准库安全检查。
220+
221+
```cmake
222+
if (CMAKE_BUILD_TYPE MATCHES "[Dd][Ee][Bb][Uu][Gg]")
223+
add_compile_definitions(-D_GLIBCXX_DEBUG)
224+
endif()
225+
```
226+
227+
#### MSVC 的标准库调试模式
228+
229+
MSVC 有两个“config”,其中 Debug config 就是标准库调试模式,会帮助你检查迭代器越界等常见未定义行为。
230+
231+
### 警告选项
232+
233+
#### GCC 建议开启的警告选项
234+
235+
建议初学者开启以下选项,可以避免很多常见错误写法,改善你的代码质量:
236+
237+
```
238+
-Wall -Wextra -Weffc++
239+
-Werror=uninitialized
240+
-Werror=return-type
241+
-Wconversion -Wsign-compare
242+
-Werror=unused-result
243+
-Werror=suggest-override
244+
-Wzero-as-null-pointer-constant
245+
-Wmissing-declarations
246+
-Wold-style-cast -Werror=vla
247+
-Wnon-virtual-dtor
248+
```
249+
250+
视频介绍:https://www.bilibili.com/video/BV1qT421a7zj
191251

192252
### C++11 ABI 问题
193253

@@ -199,4 +259,8 @@ TODO
199259
200260
或者命令行选项 `-D_GLIBCXX_USE_CXX11_ABI=0`。
201261
202-
> {{ icon.warn }} 为了更好的学习现代 C++,还是建议安装新的发行版。
262+
由于 Linux 发行版大多会捆绑特定 GCC 版本(往往是很低的版本!),为了更好的学习现代 C++,我建议:
263+
264+
1. 安装新的 Linux 发行版,最好是 Arch Linux,Tumbleweed 这类**滚动更新**的发行版,它们的所有包永久保持最新。
265+
2. 自己从源码编译 Clang,LLVM 团队打造优秀的代码品质使得 Clang 对系统底层解耦,不像 GCC 那么难伺候环境依赖。
266+
3. 或者使用 Wendous 等可以自由安装最新版 MSVC 的系统。

docs/recommend.md

+1-15
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
- parallel101/openglslides
7979
- parallel101/cppguidebook
8080
- parallel101/simdtutor
81+
- parallel101/qtguide
8182

8283
### 小彭老师自研
8384

@@ -110,18 +111,3 @@
110111
- archibate/logisim
111112
- archibate/newton
112113
- archibate/vimrc
113-
114-
## GCC 建议开启的警告选项
115-
116-
```
117-
-Wall -Wextra -Weffc++
118-
-Werror=uninitialized
119-
-Werror=return-type
120-
-Wconversion -Wsign-compare
121-
-Werror=unused-result
122-
-Werror=suggest-override
123-
-Wzero-as-null-pointer-constant
124-
-Wmissing-declarations
125-
-Wold-style-cast -Werror=vla
126-
-Wnon-virtual-dtor
127-
```

0 commit comments

Comments
 (0)