Skip to content

Commit 40f2547

Browse files
feat: updates the progresses
1 parent e602a93 commit 40f2547

24 files changed

Lines changed: 3816 additions & 40 deletions

drafts/Content-Table-Draft.md

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,59 +38,40 @@
3838

3939
- [x] 4.1 构造函数优化:初始化列表与成员初始化
4040
- [x] 4.2 移动语义(Move Semantics)在嵌入式中的应用
41-
- [ ] 4.3 RVO与NRVO(返回值优化)
42-
- [ ] 4.4 空基类优化(EBO)
43-
- [ ] 4.5 对象大小与内存对齐
44-
- [ ] 4.6 trivial类型与标准布局类型
45-
- [ ] 4.7 聚合初始化与designated initializers(C++20)
46-
47-
#### 第4章 模板与泛型编程
48-
49-
- 5.1 函数模板与类模板基础
50-
- 5.2 模板特化与偏特化
51-
- 5.3 SFINAE与编译期类型选择
52-
- 5.4 if constexpr(C++17):编译期分支
53-
- 5.5 概念(Concepts,C++20):约束模板参数
54-
- 5.6 模板实例化控制:显式实例化与extern template
55-
- 5.7 零成本策略模式实现
41+
- [x] 4.3 RVO与NRVO(返回值优化)
42+
- [x] 4.4 空基类优化(EBO)
43+
- [x] 4.5 对象大小与内存对齐,trivial类型与标准布局类型,聚合初始化与designated initializers(C++20)
5644

5745
#### 第5章 编译期编程技术
5846

59-
- `constexpr`(C++11/14/17/20)与设计技巧:常量表达式计算、lookup tables、编译期字符串处理
60-
- `consteval``constinit`(C++20)
61-
- 类型萃取(Type Traits)
62-
- 编译期查找表生成
63-
- 编译期状态机设计
64-
- 编译期单位转换与物理量计算
65-
- 模板元编程(TMP)基础与实用模式(编译期映射、选择实现)
66-
- `static_assert`、SFINAE、`if constexpr`
67-
- 练习:实现一个编译期 CRC 表或小型 FSM,并比较运行时 vs 编译期实现差异
47+
- [x] `constexpr`(C++11/14/17/20)与设计技巧:常量表达式计算、lookup tables、编译期字符串处理
48+
- [x] `consteval``constinit`(C++20)
49+
- [x] 编译期应用——查找表生成,状态机设计与单位转换与物理量计算
50+
- [x] `if constexpr`
6851

6952
------
7053

7154
## **第三部分:内存管理与资源控制**
7255

7356
#### 第6章 避免动态内存分配
7457

75-
- 7.1 动态内存的代价:碎片化与不确定性(内存布局(静态、堆、栈)、碎片化与内存对齐)
76-
- 7.2 静态存储与栈上分配策略
77-
- 7.3 对象池(Object Pool)模式
78-
- 7.4 固定池 / slab / arena 分配器实现与比较
79-
- 7.5 禁用 heap 或限制 heap 时的替代策略:放置new(Placement New)的使用
80-
- 7.6 std::array vs C数组,你们知道嘛?
81-
- 7.7 无堆容器设计
58+
- [x] 7.1 动态内存的代价:碎片化与不确定性(内存布局(静态、堆、栈)、碎片化与内存对齐)
59+
- [x] 7.2 静态存储与栈上分配策略
60+
- [x] 7.3 对象池(Object Pool)模式
61+
- [x] 7.4 固定池 / slab / arena 分配器实现与比较
62+
- [x] 7.5 禁用 heap 或限制 heap 时的替代策略:放置new(Placement New)的使用
63+
- [x] 7.6 std::array vs C数组,你们知道嘛?
8264

8365
#### 第7章 智能指针与RAII
8466

85-
- RAII 在驱动/外设管理中的应用(GPIO、SPI、DMA、文件句柄)
86-
- `unique_ptr``shared_ptr` 的嵌入式取舍(内存成本、控制周期)
87-
- intrusive 智能指针与引用计数(非堆实现)
88-
- 8.2 std::unique_ptr:零开销的独占所有权
89-
- 8.3 std::shared_ptr在嵌入式中的考虑
90-
- 8.4 自定义删除器(Custom Deleter)
91-
- 8.5 引用计数的实现与性能
92-
- 8.6 资源句柄封装:GPIO、外设、文件描述符
93-
- 8.7 作用域守卫(Scope Guard)模式
67+
- [x] RAII 在驱动/外设管理中的应用(GPIO、SPI、DMA、文件句柄)
68+
- [x] `unique_ptr``shared_ptr` 的嵌入式取舍(内存成本、控制周期)
69+
- [x] intrusive 智能指针与引用计数(非堆实现)
70+
- [x] 8.2 std::unique_ptr:零开销的独占所有权
71+
- [x] 8.3 std::shared_ptr在嵌入式中的考虑
72+
- [x] 8.4 自定义删除器(Custom Deleter)
73+
- [x] 8.5 引用计数的实现与性能
74+
- [ ] 8.6 作用域守卫(Scope Guard)模式
9475

9576
#### 第8章 容器与数据结构
9677

@@ -160,6 +141,16 @@
160141
- RTTI 的成本与替代(静态多态、CRTP)
161142
- 练习:把一个使用异常的库改造为返回 `Result<T, E>` 的风格
162143

144+
#### 第4章 模板与泛型编程
145+
146+
- 5.1 函数模板与类模板基础
147+
- 5.2 模板特化与偏特化
148+
- 5.3 SFINAE与编译期类型选择
149+
- 5.4 if constexpr(C++17):编译期分支
150+
- 5.5 概念(Concepts,C++20):约束模板参数
151+
- 5.6 模板实例化控制:显式实例化与extern template
152+
- 5.7 零成本策略模式实现
153+
163154
------
164155

165156
#### 第15章 高性能数据结构与算法(嵌入式角度)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# RVO 与 NRVO:C++ 返回值优化
2+
3+
我相信绝大多数人在写C和老C++的时候,不会喜欢返回大结构体,因为这一定会触发一次对象拷贝,对不对?但是从C++11开始,有一种看不见却又非常实在的性能魔法——返回值优化(RVO, Return Value Optimization)和命名返回值优化(NRVO, Named RVO)悄然登场。它们悄无声息地消灭了很多本可以发生的拷贝(甚至移动),让你写“自然、直观”的代码而不用为性能担忧。今天把这事讲清楚:为什么会发生,什么时候发生,哪些写法能触发(或阻止)它,最后给出实用建议,能直接贴上博客去。
4+
5+
------
6+
7+
## 试一试比较流行的TL;DR
8+
9+
- **RVO/NRVO 能避免拷贝/移动构造,直接在调用者的空间构造返回对象**,因此性能好。
10+
- **C++17 对某类返回做了“保证消除”**(即在某些返回情形下拷贝/移动被标准强制省略)。
11+
- **不要为了“显式移动”而写 `return std::move(x);`** —— 这通常会阻止 NRVO,反而更慢。
12+
- 想得到最稳妥的性能:**写自然、直观的返回本地对象的代码,信任编译器/标准**。需要针对性优化时再测和调。
13+
14+
------
15+
16+
## RVO、NRVO 究竟是什么
17+
18+
想象函数要返回一个大对象:如果按字面实现,函数会在内部构造一个临时对象,然后拷贝(或移动)到调用点;拷贝很贵。RVO/NRVO 的核心思路是,把“要返回的对象”**直接在调用者提供的内存里构造**,函数体内构造的就是最终对象——没有中间拷贝,也没有额外分配。RVO(匿名返回值优化)指的是直接返回一个临时表达式;NRVO(命名返回值优化)指的是返回函数内部的**命名局部变量**时进行的优化。
19+
20+
举个直观的对比:
21+
22+
```cpp
23+
// 假设 MyBigType 的拷贝/移动很贵
24+
MyBigType make1() {
25+
MyBigType tmp(...); // 命名局部变量 tmp
26+
// 若触发 NRVO,tmp 会在调用者的空间中直接构造
27+
return tmp; // NRVO 有机会发生
28+
}
29+
30+
MyBigType make2() {
31+
return MyBigType(...); // 直接返回临时,RVO 有机会发生
32+
}
33+
```
34+
35+
在没有任何优化时,`make1` 可能会发生一次拷贝(或移动),而有了 NRVO/RVO,拷贝/移动被省了。
36+
37+
------
38+
39+
## C++17 后的变化
40+
41+
在 C++17 之前,RVO/NRVO 是一种“允许但非必需”的优化:编译器通常会做,但标准并不强制。**从 C++17 起,标准在若干情况下要求进行拷贝消除**(例如返回 prvalue 时会被保证省略拷贝/移动)。这意味着,有些写法不再依赖编译器幸运与否:符合标准保证消除(guaranteed elision)的情形,编译器必须直接在目标处构造对象。
42+
43+
所以:在现代 C++(>= C++17)中,你能更放心地写 `return MyType(...);` 而不担心隐藏的拷贝成本。
44+
45+
------
46+
47+
## 哪些情形会阻止 NRVO / RVO?——常见踩坑
48+
49+
1. **多处 return 返回不同的局部变量**:如果函数有多个分支,每个分支返回不同的局部命名对象,编译器可能无法把它们都放在同一个目标空间,从而无法做 NRVO。
50+
2. **返回函数参数或引用**:NRVO 只针对函数内部要返回的对象,不会把函数参数“移动”成返回对象的目标。
51+
3. **对返回的局部变量做 `std::move`**`return std::move(x);` 会把 x 视为右值,从而抑制 NRVO(因为你显式表明想移动),编译器会选择移动而不是消除拷贝;通常这是退步。
52+
4. **异常控制流和复杂语义**:在某些复杂控制流或异常相关的语义下,编译器可能无法模板化地保证消除(但在 C++17 的一些常见场景,这点已被标准覆盖)。
53+
5. **编译器被禁用消除的标志**:很多编译器有关闭拷贝消除的开关(如 GCC 的 `-fno-elide-constructors`),用于测试行为;不要在发布构建中打开这个。
54+
55+
举个阻止 NRVO 的小例子:
56+
57+
```cpp
58+
MyBigType bad(bool flag) {
59+
MyBigType a(...);
60+
MyBigType b(...);
61+
if (flag) return a; // 可能无法 NRVO,因为另一分支返回不同命名对象
62+
else return b;
63+
}
64+
```
65+
66+
相比之下,下面更有利于 NRVO(或至少更简单):
67+
68+
```cpp
69+
MyBigType good(bool flag) {
70+
if (flag) return MyBigType(...); // RVO 或者 C++17 保证消除
71+
else return MyBigType(...);
72+
}
73+
```
74+
75+
------
76+
77+
## `std::move` 的误用
78+
79+
很多人习惯在返回局部变量时写 `return std::move(x);`,自以为可以强制移动从而更快。实际上:
80+
81+
- `return x;` —— 编译器**可能**做 NRVO(直接消除),也可能做移动构造(如果 NRVO 不可行)。
82+
- `return std::move(x);` —— 明确禁用了 NRVO 的机会(把 x 当作右值),编译器不得不执行移动构造。移动也有代价(尤其是当移动也要释放老资源、执行内存操作时)。因此,**不要用 `std::move` 优化 return 本地变量**,除非你对编译器行为和性能有充分测量和特殊原因。
83+
84+
简言之:`return std::move(x);` 往往是个性能反例而不是优化。
85+
86+
------
87+
88+
## 实用建议(写代码的姿势)
89+
90+
- **写直观的代码**:把函数实现写成自然的形式:构造局部对象并 `return` 它,或者直接 `return Type(args...)`。现代编译器和标准会替你把代价抹掉。
91+
- **别写 `return std::move(local);`** 来“强制”移动——这是反优化。
92+
- **在性能敏感处用基准(benchmark)说话**:不同平台、不同编译器、不同类型(移动开销 vs 拷贝开销)差别很大,遇到疑问就测。
93+
- **若必须避免拷贝,考虑传出参数或 emplace 风格**:比如 `void fill_into(MyBigType &out);` 或者 `MyBigType::create(..., std::in_place_args)`,但这应作为必要时的方案,不是常态。
94+
- **注意异常安全与语义清晰**:为了讨好 RVO 而写晦涩代码不可取,优先保证正确与易读。性能问题可在热点处剖析。
95+
96+
------
97+
98+
## 小结
99+
100+
RVO 和 NRVO 是 C++ 给我们的一份免费礼物:在不牺牲可读性的前提下,编译器能把许多看似昂贵的返回开销抹掉。自 C++17 起,某些返回情形的消除变成了语言保证,这让我们写返回值风格的接口更加安心。日常开发里,遵循“写出自然、清晰的返回语义,信任编译器”的原则;当性能成问题时,用测量代替猜测,再针对性地调整实现。
101+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# 空基类优化(EBO):C++ 的瘦身技巧
2+
3+
有一种低调而高效的内存优化,总在你看不到的地方帮你省下一点字节——**空基类优化(Empty Base Optimization, EBO)**。写库时常会用到空类作为“策略/标签/无状态的行为对象”,EBO 能把这些没有状态的基类挤出对象布局,节省空间、提升局部性。
4+
5+
------
6+
7+
## 还是试试TL;DR
8+
9+
- **EBO 允许编译器把空的基类子对象的存储省掉(即不占额外字节),从而减小派生类的 sizeof。**
10+
- **空成员变量默认不能被 EBO 压缩,但 C++20 引入的 `[[no_unique_address]]` 可以对成员实现类似的压缩效果。**
11+
- **不要依赖对象地址唯一性去识别空子对象——地址可能相同(这是被允许的优化副作用),对地址的假设会导致 bug。**
12+
- 实战:库实现常用“继承空策略类”或“compressed pair”技巧;C++20 让事情更干净,但了解传统 EBO 仍然很有用。
13+
14+
------
15+
16+
## 概念从生活化的比喻说起
17+
18+
想象一个容器对象里有两个成员:一个是真正装东西的仓库(比如 `int`、指针),另一个是空的“标签”——仅代表行为,没有数据。直觉上你可能会给每个成员都分配空间,但语言标准允许编译器把“空标签”这个基类子对象放到不占额外空间的位置(比如复用派生对象的首字节)。这样派生对象整体更小,缓存更友好——这就是 EBO 的核心。
19+
20+
标准把“最派生对象必须有非零大小”的要求施加在最派生对象上,但**基类子对象不受此限制**,编译器可以把空基类子对象大小视为 0(即不占额外字节)。这正是 EBO 的法理学来源。
21+
22+
------
23+
24+
## 简单示例
25+
26+
```cpp
27+
struct Empty {}; // 空类
28+
29+
struct A {
30+
Empty e; // 成员,通常会占 1 字节
31+
int x;
32+
};
33+
34+
struct B : Empty { // 继承 Empty —— EBO 有机会发生
35+
int x;
36+
};
37+
38+
static_assert(sizeof(A) >= sizeof(int) + 1);
39+
static_assert(sizeof(B) == sizeof(int)); // 在支持 EBO 的编译器上通常成立
40+
```
41+
42+
在上面例子中,`A` 中的 `Empty e` 是数据成员,按语言规则它需要占非零字节(以保证数组等语义);而 `B` 把 `Empty` 作为基类,编译器可以把它“压进”`B` 的布局中,从而 `sizeof(B)` 通常等于 `sizeof(int)`(不同编译器/ABI 可能细节不同)。
43+
44+
------
45+
46+
## 为什么 STL/库里常看到“继承空类”的套路?
47+
48+
标准库里,像分配器(allocator)、比较器(comparator)、删除器(deleter)等类型往往是无状态的空类。如果把它们作为成员,会浪费空间;把它们作为基类(通常是**私有继承**)可以启用 EBO,节省对象体积。很多实现把指针+空删除器这种情况包装成“compressed pair”或类似工具,以实现最小化的对象大小。微软的 STL 博客和其他实现说明了这种做法的普遍性。
49+
50+
------
51+
52+
## C++20:`[[no_unique_address]]` 把“空成员优化”变得正式且安全
53+
54+
传统 EBO 只能通过继承实现(成员无法被压缩)。C++20 引入的 `[[no_unique_address]]` 属性允许**成员**也能被允许与其它子对象共享地址(即允许零大小语义),从而用成员语法就能达到类似 EBO 的效果,代码更直观、语义更清晰。例如:
55+
56+
```cpp
57+
struct Empty {};
58+
struct Holder {
59+
[[no_unique_address]] Empty e; // 现在可以和其它成员共享地址
60+
int x;
61+
};
62+
```
63+
64+
这在实现上比私有继承更好看,也避免了继承带来的潜在接口暴露。cppreference 和一些实现文章对 `[[no_unique_address]]` 的语义与限制有总结,强烈建议在能用 C++20 的地方优先采用。
65+
66+
------
67+
68+
## 常见误解与踩坑(务必注意)
69+
70+
- **“空类子对象一定没有地址”——错。** 标准允许基类子对象与最派生对象共享起始地址;这会导致基类子对象的地址可能与其它子对象(或对象整体)相同。不要写依赖子对象地址唯一性的代码。
71+
- **`std::pair` 为何不能直接利用 EBO?** 因为 `std::pair``first``second` 作为**成员**而不是空基类,因此传统 EBO 无法用于成员(除非使用 `[[no_unique_address]]` 或把实现改成 compressed-pair 风格)。这也是为啥有 “compressed pair” 之类的内部实现技巧。
72+
- **多个空基类有时会互相影响**:若你从多个空类型继承,编译器会尝试为它们做 EBO,但在某些情况下(比如重复的基类类型、ABI 或嵌套模板导致的类型相同)会限制优化。常见的做法是让每个空基类的类型对编译器来说“独一无二”(例如通过模板参数化)以确保压缩生效。有人把这个问题称为“需要使基类类型分别化”。
73+
74+
------
75+
76+
## 实战建议
77+
78+
1. **默认不用过早优化**:把策略类写成空类、用成员或继承都行;可读性优先。
79+
2. **若需要最小内存或实现库(如智能指针、容器),优先考虑 `[[no_unique_address]]`(C++20)或受控的私有继承 EBO 技巧。** C++20 能让代码更直观。
80+
3. **别依赖对象或子对象地址唯一性**:在写调试、序列化或比较逻辑时,避免用地址来区分空子对象。地址可能会相同,标准允许这种重用。
81+
82+
------
83+
84+
## 小结
85+
86+
EBO 是 C++ 里一门“看得见效果却不显山露水”的微优化:让空策略类不再浪费字节。历史上我们用私有继承实现 EBO,现代 C++(C++20)通过 `[[no_unique_address]]` 让空成员也能被压缩,代码更直观更安全。实际工程里优先写清晰可维护的代码:当对象大小敏感时,再用 EBO / `[[no_unique_address]]` / compressed-pair 等技巧去手工优化,并在目标编译器上验证行为。

0 commit comments

Comments
 (0)