Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/rules/documents-frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ globs: documents/**/*.md

| 字段 | 类型 | 说明 |
|------|------|------|
| `description` | string | 一句话摘要,缺失时由 `documents/hooks/meta.py` 自动生成,但建议手动填写以控制质量 |
| `description` | string | 一句话摘要,建议手动填写以控制质量 |
| `tags` | list[string] | 分类标签,必须来自下方 VALID_TAGS 集合 |

Expand Down
6 changes: 0 additions & 6 deletions .env.example

This file was deleted.

4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.claude/prompts/vol5-code-verification.md
.claude/chapter-projects-outline.md
.claude/templates/
.claude/tools/

# Build directories
build/
Expand All @@ -28,8 +29,5 @@ __pycache__/
# cppreference crawler cache (contains API keys in env)
scripts/cppref_cache/

# Environment variables
.env

# Ignore Any Caches brought from local
.cache/
18 changes: 1 addition & 17 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,9 @@ repos:
files: '^documents/.*\.md$'
exclude: '^documents/.*\bindex\.md$'

# Frontmatter validation (local hook)
# Local hooks
- repo: local
hooks:
- id: validate-frontmatter
name: Validate article frontmatter
entry: scripts/validate_frontmatter.py
language: python
additional_dependencies: ['pyyaml']
files: '^documents/core-embedded-cpp/.*\.md$'
exclude: '(index\.md|tags\.md)$'
pass_filenames: false

- id: clang-format
name: clang-format C/C++ sources
entry: clang-format -i
Expand All @@ -35,13 +26,6 @@ repos:
always_run: true
pass_filenames: false

- id: check-bold-rendering
name: Check bold rendering (**)
entry: pnpm exec tsx scripts/check_bold_rendering.ts
language: system
files: '^documents/.*\.md$'
pass_filenames: false

# Check for added large files
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ sudo apt update
Then install all the packages we need in one go:

```bash
sudo apt install gcc-arm-none-eabi gdb-arm-none-eabi openocd cmake build-essential
sudo apt install gcc-arm-none-eabi gdb-multiarch openocd cmake build-essential
```

Let me explain what these packages do. `gcc-arm-none-eabi` is a big gift pack containing the cross-compiler, linker, `objcopy`, `size`, and a whole set of tools. `gdb-arm-none-eabi` is the ARM version of GDB for debugging embedded programs. `openocd` we mentioned earlier, for flashing and GDB Server. `cmake` and `build-essential` are build tools, with the latter containing basic compilation tools like `make`.
Let me explain what these packages do. `gcc-arm-none-eabi` is a big gift pack containing the cross-compiler, linker, `objcopy`, `size`, and a whole set of tools. `gdb-multiarch` is the multi-architecture GDB—it can debug ARM as well as RISC-V and other architectures. Note that the executable it installs is called `gdb-multiarch`, not `arm-none-eabi-gdb` (Ubuntu removed the legacy `gdb-arm-none-eabi` package from the repos a while ago). So wherever this tutorial says `arm-none-eabi-gdb`, Ubuntu users should substitute `gdb-multiarch` at the command line; Arch users keep `arm-none-eabi-gdb`. `openocd` we mentioned earlier, for flashing and GDB Server. `cmake` and `build-essential` are build tools, with the latter containing basic compilation tools like `make`.

After installation, we can verify if the toolchain is actually installed:

Expand Down Expand Up @@ -125,7 +125,7 @@ The installation command is a bit shorter than Ubuntu's:
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-gdb openocd cmake
```

Here is a difference from Ubuntu: Arch splits the tools into multiple packages. `arm-none-eabi-gcc` is the compiler itself, `arm-none-eabi-binutils` contains `ld`, `objcopy`, `size`, and other tools, and `arm-none-eabi-gdb` is the debugger. Ubuntu packs all of these into `gcc-arm-none-eabi`, so fewer packages need to be installed.
Here is a difference from Ubuntu: Arch splits the tools into multiple packages. `arm-none-eabi-gcc` is the compiler itself, `arm-none-eabi-binutils` contains `ld`, `objcopy`, `size`, and other tools, and `arm-none-eabi-gdb` is the debugger. Ubuntu bundles the compiler and binutils into `gcc-arm-none-eabi`, while the debugger is the separate multi-architecture package `gdb-multiarch`—so the debugger command differs between the two distros (Arch: `arm-none-eabi-gdb`, Ubuntu: `gdb-multiarch`), as noted in the Ubuntu section above.

Verify if the installation was successful:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@ title: 结构化绑定:一行解包多个值
---
# 结构化绑定:一行解包多个值

笔者在写代码的时候,经常遇到一个很别扭的场景:函数返回了多个值,你得一个个拆开赋给变量。用 `pair` 的时候写 `result.first`、`result.second`,用 `tuple` 的时候写 `std::get<0>(t)` —— 要么语义不明,要么写法丑陋。C++11 引入了 `std::tie` 来缓解这个问题,但老实说,那语法也不算优雅:你得先声明好所有变量,再用 `tie` 往里塞。有没有那种,跟Python返回多个值的拆分写法一样爽的feature呢? 真有了孩子们!
笔者写代码时总撞上一个别扭的场景:函数返回多个值,你得一个个拆开赋给变量。用 `pair` `result.first`、`result.second`,用 `tuple` `std::get<0>(t)`——要么语义不明,要么写法丑陋。C++11 引入了 `std::tie` 缓解这事,但老实说那语法也不优雅:先声明好所有变量,再用 `tie` 往里塞。有没有那种跟 Python `a, b = func()` 一样爽的拆分写法?真有了,孩子们。

C++17 终于给了我们一个真正的答案——结构化绑定(Structured Binding)。一行代码把 `pair`、`tuple`、数组、结构体全部拆开,直接拿到有名字的变量,语义清晰、零开销。

> 一句话总结:**结构化绑定让你把复合类型"解包"到多个命名变量中,编译器在幕后帮你完成一切。**
C++17 终于给了正经答案——结构化绑定(Structured Binding)。一行把 `pair`、`tuple`、数组、结构体全拆开,直接拿到有名字的变量,语义清晰,零开销。

------

## 第一步——绑定 pair 和 tuple
## pair 和 tuple 讲起

### pair:最常见的多返回值

Expand Down Expand Up @@ -64,7 +62,14 @@ for (const auto& [id, name] : sensor_names) {
}
```

> 为什么要写 `+id`?因为 `uint8_t` 的 `operator<<` 会把它当字符输出,`+` 会做整型提升(integral promotion),强制转换成 `int` 再打印。
这里有个细节:循环体里写的是 `+id` 而不是 `id`。原因在于 `uint8_t` 的 `operator<<` 会把它当字符输出,而 `+` 会做整型提升(integral promotion),强制转换成 `int` 再打印。口说无凭,跑一下最直白(GCC 16.1.1,`-O2`):

```text
without + (raw uint8_t): A
with + (promoted) : 65
```

同样是 `id = 65`,不加 `+` 打印出来是字符 `A`,加了才显示数字。

### tuple:超过两个值的情况

Expand Down Expand Up @@ -93,7 +98,7 @@ std::tie(record_id, name, value) = query_database(42);

------

## 第二步——绑定原生数组和结构体
## 原生数组与结构体

### 原生数组

Expand All @@ -107,7 +112,9 @@ auto [r, g, b] = rgb;
二维数组的每一行也可以在循环中解包:

```cpp
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int matrix[2][3] = {
{1, 2, 3}, {4, 5, 6}
};
for (auto& row : matrix) {
auto [a, b, c] = row;
std::cout << a << ' ' << b << ' ' << c << '\n';
Expand All @@ -132,13 +139,13 @@ SensorReading reading{5, 23.5f, 1234567890, true};
auto [id, val, ts, valid] = reading;
```

这恐怕是结构化绑定最直观的用法了。你甚至不需要理解任何模板元编程,只要结构体成员是公有的就能用。
不需要懂任何模板元编程,只要结构体成员是公有的就能用——这恐怕是结构化绑定最直观的用法了

结构化绑定要求数据成员按声明顺序绑定,且完全支持位域(bit field)。如果结构体里有 `mutable` 成员,行为可能需要注意:绑定到的"匿名变量"可能被 `const` 修饰,但 `mutable` 成员不受此限制,仍可修改。

------

## 第三步——理解绑定的三种语义
## 三种绑定语义

结构化绑定并不总是拷贝。实际上,`auto` 前面的修饰符决定了底层匿名变量的类型:

Expand All @@ -147,7 +154,7 @@ auto [id, val, ts, valid] = reading;
- **`const auto& [...]`**——绑定到 const 左值引用。只读访问,不拷贝。
- **`auto&& [...]`**——转发引用。既能绑定左值也能绑定右值。

来看一个例子来区分它们
用一个例子区分它们

```cpp
std::pair<int, int> range{1, 10};
Expand All @@ -160,6 +167,13 @@ auto& [r3, r4] = range;
r3 = 5; // range.first 变成 5
```

跑一下就能看到 `auto&` 改的是原对象、`auto` 改的是拷贝:

```text
range.first after auto& mutation: 5
r1 (copy, unaffected) : 1
```

底层机制是这样的:编译器先声明一个匿名变量(类型由 `auto`/`auto&`/`const auto&`/`auto&&` 决定),用右侧表达式初始化它。然后每个绑定变量都是这个匿名变量的成员的引用(或者对于按值情况,是指向拷贝的成员的引用)。

```cpp
Expand All @@ -186,7 +200,7 @@ auto [x, y] = std::make_pair(1, 2);

------

## 第四步——自定义类型的绑定支持(Tuple-Like Protocol
## 让自定义类型支持绑定:Tuple-Like Protocol

如果你的类有私有成员,不能直接用结构体方式解绑。但 C++ 提供了另一条路:让编译器把你的类当作 "tuple-like" 类型来处理。只需要三样东西:

Expand Down Expand Up @@ -231,22 +245,28 @@ template<>
struct std::tuple_element<1, SensorData> { using type = float; };
```

现在就可以愉快地解包了:
配合 `get<I>` 的 ADL 重载,现在就可以愉快地解包了:

```cpp
SensorData data{5, 23.5f};
auto [id, value] = data; // id = 5, value = 23.5
```

实跑确认(注意 `id` 又得加 `+` 才打印成数字):

```text
id = 5, value = 23.5
```

> 这里的关键是 `get<I>()` 函数必须定义在类所在的命名空间中(ADL 规则),这样编译器才能找到它。对于标准命名空间 `std` 中的特化,你需要在 `std` 命名空间中写 `tuple_size` 和 `tuple_element` 的特化,但 `get` 函数放在类所在的命名空间即可。

这套机制被称为 "tuple-like protocol",标准库的 `std::pair`、`std::tuple`、`std::array` 都是靠它实现结构化绑定支持的。

------

## C++20 的增强
## C++20 的变化

C++20 对结构化绑定做了一些增强,主要与 constexpr 上下文相关。
C++20 对结构化绑定做了几处调整,主要与 constexpr 上下文相关。

结构化绑定可以在 `constexpr` 函数内部使用,这意味着编译期计算函数也能返回多值并用结构化绑定接收:

Expand Down Expand Up @@ -284,7 +304,7 @@ C++20 新增的是初始化捕获语法(`key = k`),这在某些情况下

## 性能:零开销的语法糖

结构化绑定本身没有任何运行时开销。它纯粹是编译期的语法变换——编译器会在幕后创建匿名变量,然后让绑定变量引用匿名变量的成员。生成的汇编代码和你手写的"取出成员再赋值"完全一致。
结构化绑定本身没有任何运行时开销。它纯粹是编译期的语法变换——编译器会在幕后创建匿名变量,然后让绑定变量引用匿名变量的成员。

```cpp
// 这两种写法生成的汇编代码完全一样
Expand All @@ -296,7 +316,23 @@ auto x = __tmp.first;
auto y = __tmp.second;
```

性能方面的建议很简单:对于大结构体用 `const auto&` 避免拷贝,对于小型类型(内置类型、小 struct)直接用 `auto` 按值拷贝。`auto&&` 在泛型代码中很有用,但在具体类型已知的场景下,明确写 `auto` 或 `const auto&` 更清晰。
"汇编完全一样"这种话不能空口说。拿 GCC 16.1.1 实测,两种写法各 `g++ -std=c++17 -O2 -S`,再 `diff`:

```bash
g++ -std=c++17 -O2 -S sb_structured.cpp
g++ -std=c++17 -O2 -S sb_manual.cpp
diff sb_structured.s sb_manual.s
```

`diff` 输出只有一行——`.file` 那行的源文件名不同,实际指令完全一致:

```text
_Z1fv: # f(),两个版本一模一样
movl $7, %eax # 直接返回 3 + 4 = 7
ret
```

编译器把 `get_point()` 内联后直接常量折叠成 `movl $7, %eax`,结构化绑定没留下任何痕迹。所以性能建议很简单:大结构体用 `const auto&` 避免拷贝,小类型(内置类型、小 struct)直接 `auto` 按值拷贝。`auto&&` 在泛型代码里有用,但具体类型已知时,明确写 `auto` 或 `const auto&` 更清晰。

------

Expand Down Expand Up @@ -350,11 +386,11 @@ class MyClass {
allow-run
/>

## 小结
## 收尾

结构化绑定是 C++17 中最实用的特性之一。它支持的类型覆盖了日常开发的绝大部分场景:`pair`、`tuple`、原生数组、公有成员结构体,以及实现了 tuple-like protocol 的自定义类型。绑定的语义完全由 `auto` 前面的修饰符决定——`auto` 是拷贝,`auto&` 是引用,`const auto&` 是只读引用,`auto&&` 是转发引用
结构化绑定能覆盖的类型就这些:`pair`、`tuple`、原生数组、公有成员结构体,外加实现了 tuple-like protocol 的自定义类型。语义全由 `auto` 前面的修饰符决定——`auto` 拷贝、`auto&` 引用、`const auto&` 只读引用、`auto&&` 转发引用

在实战中,笔者最常用的是在范围 for 中遍历 map(`for (const auto& [k, v] : m)`)和处理多返回值函数。配合下一章要讲的 if/switch 初始化器,结构化绑定能让代码的简洁度和可读性都上一个台阶
笔者日常用得最多的还是范围 for 遍历 map(`for (const auto& [k, v] : m)`)和接多返回值函数。下一章的 if/switch 初始化器跟它配套,两者一起用,代码能再瘦一圈

## 参考资源

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ sudo apt update
```bash
sudo apt install -y \
gcc-arm-none-eabi \
gdb-arm-none-eabi \
gdb-multiarch \
openocd \
cmake \
build-essential
```

让我解释一下这几个包都干嘛的。`gcc-arm-none-eabi` 是个大礼包,里面包含了交叉编译器、链接器、objcopy、size 等一整套工具。`gdb-arm-none-eabi` 是 ARM 版本的 GDB,用来调试嵌入式程序。`openocd` 我们前面说过了,是烧录和 GDB Server。`cmake` 和 `build-essential` 则是构建工具,后者包含了 make 等基础编译工具。
让我解释一下这几个包都干嘛的。`gcc-arm-none-eabi` 是个大礼包,里面包含了交叉编译器、链接器、objcopy、size 等一整套工具。`gdb-multiarch` 是多架构版的 GDB,能调 ARM 也能调 RISC-V 之类的其他架构——注意它装出来的可执行文件就叫 `gdb-multiarch`,不是 `arm-none-eabi-gdb`(Ubuntu 早就把 `gdb-arm-none-eabi` 这个老包从源里移除了)。所以本教程里凡是写到 `arm-none-eabi-gdb` 的地方,Ubuntu 用户在命令行里替换成 `gdb-multiarch` 即可;Arch 用户保持 `arm-none-eabi-gdb`。`openocd` 我们前面说过了,是烧录和 GDB Server。`cmake` 和 `build-essential` 则是构建工具,后者包含了 make 等基础编译工具。

安装完成之后,我们可以验证一下工具链是不是真的装上了:

Expand Down Expand Up @@ -122,7 +122,7 @@ This is free software; see the source for copying conditions. There is no warra
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils arm-none-eabi-gdb openocd cmake make
```

这里有个和 Ubuntu 不同的地方:Arch 把工具拆分成了多个包。`arm-none-eabi-gcc` 是编译器本身,`arm-none-eabi-binutils` 包含了 ld、objcopy、size 这些工具,`arm-none-eabi-gdb` 是调试器。Ubuntu 把这些都打包进了 `gcc-arm-none-eabi`,所以需要装的包更少
这里有个和 Ubuntu 不同的地方:Arch 把工具拆分成了多个包。`arm-none-eabi-gcc` 是编译器本身,`arm-none-eabi-binutils` 包含了 ld、objcopy、size 这些工具,`arm-none-eabi-gdb` 是调试器。Ubuntu 那边把编译器和 binutils 合在了 `gcc-arm-none-eabi` 里,调试器则是单独的多架构包 `gdb-multiarch`——所以两边调试器命令名不一样(Arch 是 `arm-none-eabi-gdb`,Ubuntu 是 `gdb-multiarch`),见前面 Ubuntu 段的说明

验证一下安装是否成功:

Expand Down
Loading
Loading