@chengzhao-dev/dsh-cpp-workflow 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +106 -0
- package/README.md +105 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +7 -0
- package/lib/index.js +203 -0
- package/lib/workspace.js +168 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +80 -4
- package/skills/building-cpp/SKILL.md +44 -0
- package/skills/building-cpp/assets/cmake/applying-warnings.cmake +10 -0
- package/skills/building-cpp/assets/cmake/fetching-googletest.cmake +15 -0
- package/skills/building-cpp/assets/cmake/setting-rpath.cmake +8 -0
- package/skills/building-cpp/assets/config/.clang-format +40 -0
- package/skills/building-cpp/assets/config/.clang-tidy +92 -0
- package/skills/building-cpp/assets/config/.clangd +12 -0
- package/skills/building-cpp/assets/config/.vscode/extensions.json +6 -0
- package/skills/building-cpp/assets/config/.vscode/settings.json +34 -0
- package/skills/building-cpp/assets/templates/projects/common/running-build.sh +152 -0
- package/skills/building-cpp/assets/templates/projects/multi/CMakeLists.txt +18 -0
- package/skills/building-cpp/assets/templates/projects/multi/include/greeting.h +10 -0
- package/skills/building-cpp/assets/templates/projects/multi/src/greeting.cpp +13 -0
- package/skills/building-cpp/assets/templates/projects/multi/src/main.cpp +8 -0
- package/skills/building-cpp/assets/templates/projects/shared-library/CMakeLists.txt +24 -0
- package/skills/building-cpp/assets/templates/projects/shared-library/greeting/CMakeLists.txt +5 -0
- package/skills/building-cpp/assets/templates/projects/shared-library/greeting/include/greeting.h +10 -0
- package/skills/building-cpp/assets/templates/projects/shared-library/greeting/src/greeting.cpp +13 -0
- package/skills/building-cpp/assets/templates/projects/shared-library/src/main.cpp +8 -0
- package/skills/building-cpp/assets/templates/projects/single/CMakeLists.txt +14 -0
- package/skills/building-cpp/assets/templates/projects/single/src/main.cpp +9 -0
- package/skills/building-cpp/assets/templates/projects/static-library/CMakeLists.txt +18 -0
- package/skills/building-cpp/assets/templates/projects/static-library/greeting/CMakeLists.txt +5 -0
- package/skills/building-cpp/assets/templates/projects/static-library/greeting/include/greeting.h +10 -0
- package/skills/building-cpp/assets/templates/projects/static-library/greeting/src/greeting.cpp +13 -0
- package/skills/building-cpp/assets/templates/projects/static-library/src/main.cpp +8 -0
- package/skills/building-cpp/examples/cmake.md +150 -0
- package/skills/building-cpp/examples/environment.md +125 -0
- package/skills/building-cpp/examples/tooling.md +155 -0
- package/skills/building-cpp/references/build-script.md +63 -0
- package/skills/building-cpp/references/cmake.md +168 -0
- package/skills/building-cpp/references/environment.md +104 -0
- package/skills/building-cpp/references/linking.md +101 -0
- package/skills/building-cpp/references/static-analysis.md +136 -0
- package/skills/building-cpp/scripts/checking-environment.mjs +498 -0
- package/skills/building-cpp/steps/01-build.md +76 -0
- package/skills/building-cpp/steps/02-tooling.md +41 -0
- package/skills/building-cpp/steps/03-check.md +60 -0
- package/skills/developing-cpp/SKILL.md +42 -0
- package/skills/developing-cpp/examples/comments.md +231 -0
- package/skills/developing-cpp/examples/exceptions.md +134 -0
- package/skills/developing-cpp/examples/naming.md +157 -0
- package/skills/developing-cpp/examples/structure.md +79 -0
- package/skills/developing-cpp/references/comments.md +123 -0
- package/skills/developing-cpp/references/declaration-order.md +97 -0
- package/skills/developing-cpp/references/exceptions.md +79 -0
- package/skills/developing-cpp/references/glossary.md +88 -0
- package/skills/developing-cpp/references/google-cpp-style.md +109 -0
- package/skills/developing-cpp/references/language.md +119 -0
- package/skills/developing-cpp/references/layout.md +73 -0
- package/skills/developing-cpp/references/naming.md +62 -0
- package/skills/developing-cpp/references/performance.md +82 -0
- package/skills/developing-cpp/references/project-design.md +79 -0
- package/skills/developing-cpp/references/standard-library.md +100 -0
- package/skills/developing-cpp/references/types.md +147 -0
- package/skills/developing-cpp/steps/01-behavior.md +36 -0
- package/skills/developing-cpp/steps/02-boundary.md +40 -0
- package/skills/developing-cpp/steps/03-declaration.md +32 -0
- package/skills/developing-cpp/steps/04-implementation.md +33 -0
- package/templates/cpp-workflow-project/SKILL.md +49 -0
- package/templates/cpp-workflow-project/environment.json +5 -0
- package/templates/cpp-workflow-project/stages/stage-table.md +4 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 源码布局
|
|
2
|
+
|
|
3
|
+
布局服务于边界:库是纯的,程序是薄的,测试从公共接口进入。这里只定目录和文件职责;CMake target 在 `building-cpp` 的定构建。
|
|
4
|
+
|
|
5
|
+
## 组件
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
<component>/
|
|
9
|
+
include/
|
|
10
|
+
<name>.h 公共接口,裸文件名包含
|
|
11
|
+
src/
|
|
12
|
+
<name>.cpp 实现,可以包含私有头
|
|
13
|
+
<detail>.h 私有头,只被本组件 src 包含
|
|
14
|
+
tests/
|
|
15
|
+
<name>_test.cpp 通过公共接口测试
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- 公共头直接放 `include/` 根下;需要分组时用子目录,但包含路径仍从公共根写起。
|
|
19
|
+
- 私有头放 `src/`,不进入公共 include 根;测试不得包含私有头。
|
|
20
|
+
- 一个组件一个职责;组件名用 snake_case,和命名空间一致。
|
|
21
|
+
- 组件之间通过公共头依赖;不要用 `../other/src/private.h` 跨组件包含私有实现。
|
|
22
|
+
|
|
23
|
+
## 程序
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
app/
|
|
27
|
+
include/
|
|
28
|
+
cli.h 输入输出与参数解析的私有接口
|
|
29
|
+
src/
|
|
30
|
+
cli.cpp 终端交互
|
|
31
|
+
main.cpp 只做装配:构造依赖、调用 Run、翻译退出码
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `main.cpp` 不写业务规则;它只解析参数、打开流、调用库、捕获边界异常。
|
|
35
|
+
- 终端输入输出集中在 `app/`;库不包含 `<iostream>`,也不打印。
|
|
36
|
+
- 只有一个程序时 `app/` 可以退化成根 `src/main.cpp`;一旦需要测试交互逻辑,再拆 `cli.h` / `cli.cpp`。
|
|
37
|
+
- 程序可以依赖库;库不能反向依赖程序。
|
|
38
|
+
|
|
39
|
+
## 单文件与多文件
|
|
40
|
+
|
|
41
|
+
| 形态 | 何时用 | 布局 |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| 单文件 | 一次性脚本、极小示例 | `src/main.cpp`,辅助函数放匿名命名空间 |
|
|
44
|
+
| 多文件单程序 | 逻辑开始分块,但还没有复用方 | `src/*.cpp` + `include/*.h`,一个 `app` target |
|
|
45
|
+
| 静态库 + 程序 | 需要测试、需要被多个程序链接 | `<component>/` + `app/`,库 target + `app` target |
|
|
46
|
+
| 动态库 + 程序 | 需要运行时替换实现、插件式加载 | 同上,库 target 改 `SHARED`,注意运行时库路径 |
|
|
47
|
+
|
|
48
|
+
不要为了「以后可能复用」提前拆库;出现第二个调用方或测试需要直接调用时再拆。拆的时候接口不变,只移动文件。
|
|
49
|
+
|
|
50
|
+
## 头文件契约
|
|
51
|
+
|
|
52
|
+
- 公共头自包含:单独编译一个只包含它的 `.cpp` 必须通过。
|
|
53
|
+
- 头文件不定义非 inline 的普通全局变量;共享常量用 `inline constexpr`。
|
|
54
|
+
- 头文件不写 `using namespace`;不污染全局命名空间。
|
|
55
|
+
- 模板和 `constexpr` 的实现可以留在头文件;普通函数体放 `.cpp`。
|
|
56
|
+
- 头文件不包含 `<iostream>`;需要流时在 `.cpp` 或程序层包含。
|
|
57
|
+
|
|
58
|
+
## 测试
|
|
59
|
+
|
|
60
|
+
- 测试文件放组件 `tests/` 或顶层 `tests/`,命名 `<被测>_test.cpp`。
|
|
61
|
+
- 测试只包含公共头;需要测私有逻辑时,先判断它是否应该成为公共行为。
|
|
62
|
+
- 一个测试文件对应一个行为组;测试名描述行为,不描述实现。
|
|
63
|
+
- 测试不复制实现,不依赖执行顺序,不读写固定绝对路径。
|
|
64
|
+
|
|
65
|
+
## 迁移
|
|
66
|
+
|
|
67
|
+
从单文件迁到库 + 程序时:
|
|
68
|
+
|
|
69
|
+
1. 先定边界(第 2 步),列出公共头。
|
|
70
|
+
2. 移动文件,不改行为;测试保持绿色。
|
|
71
|
+
3. 再改 CMake target(`building-cpp` 的定构建),最后跑 `building-cpp` 的通过检查。
|
|
72
|
+
|
|
73
|
+
一次只做一种迁移;不要把「拆库 + 改名 + 改行为」混在一起。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 命名
|
|
2
|
+
|
|
3
|
+
命名先回答「它是什么角色」,再选词。规则表是底线,语义角色表决定用哪个动词。
|
|
4
|
+
|
|
5
|
+
## 规则表
|
|
6
|
+
|
|
7
|
+
| 种类 | 规则 | 好 | 差 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| 类型 / 类 / 结构 / 枚举 | PascalCase | `TaskTable`, `Order`, `TaskState` | `task_table_t`, `order_result` |
|
|
10
|
+
| 函数 / 成员函数 | PascalCase | `Contains`, `GetTaskState` | `contains`, `gettaskstate` |
|
|
11
|
+
| 变量 / 参数 | snake_case | `task_count`, `order` | `TaskCount`, `o` |
|
|
12
|
+
| 类成员 | snake_case + `_` | `tasks_`, `orders_` | `m_tasks`, `tasks` |
|
|
13
|
+
| 常量 / 枚举子 | `k` + PascalCase | `kTaskCount`, `Order::kDraft` | `TASK_COUNT`, `Draft` |
|
|
14
|
+
| 命名空间 | snake_case | `taskflow`, `warehouse` | `Taskflow`, `Warehouse` |
|
|
15
|
+
| 宏 | 全大写 + 项目前缀,尽量不用 | `CPP_WORKFLOW_ASSERT` | `ASSERT` |
|
|
16
|
+
|
|
17
|
+
- 文件名用 snake_case:`order_result.h`、`task_table.cpp`;测试文件 `task_table_test.cpp`。
|
|
18
|
+
- 布尔变量用 `is_` / `has_` / `can_` 开头:`is_valid`、`has_stock`;不要用 `flag`、`status`。
|
|
19
|
+
- 容器用复数:`tasks`、`orders`;单个元素用单数。
|
|
20
|
+
- 缩写按单词处理:`url`、`id`、`http`,不要写 `URL` 混进 snake_case;类型名里可以 `HttpClient`。
|
|
21
|
+
- 不要用单字母名,循环下标 `i` / `j` 除外;不要用 `data`、`info`、`tmp` 这类不说明内容的名字。
|
|
22
|
+
|
|
23
|
+
## 语义角色
|
|
24
|
+
|
|
25
|
+
| 角色 | 前缀 / 动词 | 例子 | 说明 |
|
|
26
|
+
| --- | --- | --- | --- |
|
|
27
|
+
| 查询 | `Get` | `GetCurrentOrder()` | 只读、不改变状态;返回成员时按 `const` 引用 |
|
|
28
|
+
| 判断 | `Is` / `Has` / `Can` | `Contains()`, `HasStock()` | 返回 `bool`,不抛异常 |
|
|
29
|
+
| 查找 | `Find` | `FindTask()` | 找不到是正常结果,返回 `optional` 或迭代器,不抛 |
|
|
30
|
+
| 计数 | `Count` | `CountPendingTasks()` | 返回数量,不修改状态 |
|
|
31
|
+
| 尝试 | `Try` | `TryReserve()` | 可能失败但不抛异常,返回结果类型 |
|
|
32
|
+
| 确定性动作 | 领域动词 | `CompleteTask()`, `Reset()` | 前置条件满足时一定成功;违反合同才抛 |
|
|
33
|
+
| 计算 | 动词开头 | `ScoreOrder()` | 从输入算出结果,不修改外部状态 |
|
|
34
|
+
| 映射 | `<来源>To<目标>` | `OrderToReceipt()` | 名字必须含两端角色 |
|
|
35
|
+
| 读写 | `Read` / `Write` | `ReadTaskTable()`, `WriteTaskTable()` | 涉及 I/O 时用;不要在库核心用 |
|
|
36
|
+
| 访问器 | `Get` + PascalCase | `GetHistory()` | 不用成员名直接当函数名 |
|
|
37
|
+
|
|
38
|
+
- 返回 `bool` 的 lambda 也用 `is_` / `has_` / `can_` 命名:`auto is_empty = [](const Order& order) { ... };`。
|
|
39
|
+
- `Find` 不把“没找到”当异常;`Get` 在前置条件不满足时可以抛 `std::out_of_range`。两者不要互换。
|
|
40
|
+
- 查询返回多个值时用 `struct`,不要用三个 `GetXxx()` 加输出参数。
|
|
41
|
+
|
|
42
|
+
- 禁止裸 `To()` / `Of()`:调用点看不出方向。
|
|
43
|
+
- 禁止 `Set` + 成员名的伪访问器;需要修改时用领域动作(`CompleteTask` 而不是 `SetItem`)。
|
|
44
|
+
- `current_` 前缀只在有多个候选、且「当前」可能混淆时使用:`current_order_`。只有一个当前对象时不要加。
|
|
45
|
+
|
|
46
|
+
## 类型命名的判断顺序
|
|
47
|
+
|
|
48
|
+
1. 纯数据、无 invariant、可以直接聚合初始化 → `struct`,名字是名词:`Order`, `TaskRecord`。
|
|
49
|
+
2. 有 invariant、需要构造函数维持 → `class`,名字是名词:`TaskTable`, `Warehouse`。
|
|
50
|
+
3. 封闭的状态集合 → `enum class`,名字是单数名词:`TaskState`, `OrderResult`;枚举子用 `k` 前缀。
|
|
51
|
+
4. 只有行为、没有状态,且调用方需要替换实现 → 抽象基类;只有一个实现时先用普通类。
|
|
52
|
+
5. 一组自由函数 → 命名空间 + 动词;不要为了「像 Java」造一个 `XxxUtil` 类。
|
|
53
|
+
|
|
54
|
+
## 改名
|
|
55
|
+
|
|
56
|
+
- 改名是一次独立改动:先搜全部调用点,再改声明、定义、测试和注释。
|
|
57
|
+
- 不要把改名和改行为混在一个提交里;改名后测试必须仍然绿色且行为句不变。
|
|
58
|
+
- 公共 API 改名视为破坏性变更;需要兼容时保留旧名转发,并在注释里写弃用期限。
|
|
59
|
+
|
|
60
|
+
## 术语
|
|
61
|
+
|
|
62
|
+
中文术语与英文标识符的对应见 `references/glossary.md`。代码标识符用英文;注释可以用中文,但同一个概念只用一种译法。
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 性能与常见陷阱
|
|
2
|
+
|
|
3
|
+
先证明慢在哪里,再改代码。没有测量的“优化”通常只是把复杂度搬了个位置。
|
|
4
|
+
|
|
5
|
+
## 测量顺序
|
|
6
|
+
|
|
7
|
+
1. 写清可观察的性能目标:延迟、吞吐、内存上限,或“输入规模翻倍时耗时如何增长”。
|
|
8
|
+
2. 用真实规模的输入测量;玩具数据上的常数差异没有意义。
|
|
9
|
+
3. 先看算法复杂度,再看内存访问,再看常数因子。
|
|
10
|
+
4. 用 profiler 找热点;不要凭代码长短猜。
|
|
11
|
+
5. 改一处,复测同一指标;记录测量条件和结果。
|
|
12
|
+
|
|
13
|
+
- 发布构建(`-O2` / `-O3`)测性能,调试构建测正确性。
|
|
14
|
+
- 微基准要防止编译器把结果优化掉;使用输入、累加结果并输出。
|
|
15
|
+
- 单次测量受缓存、频率、调度影响;取多次的中位数或分位数,不只报最好的一次。
|
|
16
|
+
|
|
17
|
+
## 复杂度与数据结构
|
|
18
|
+
|
|
19
|
+
- 先问操作的渐进复杂度:`vector` 尾部追加摊还 O(1),中间插入 O(n);`map` 查找 O(log n);`unordered_map` 平均 O(1)、最坏 O(n)。
|
|
20
|
+
- 嵌套循环里做线性查找通常是把 O(n) 变成 O(n²);先建索引或排序。
|
|
21
|
+
- 频繁按 key 查找时,`unordered_map` 通常比 `map` 快,但迭代顺序不稳定;需要有序输出时不要选它。
|
|
22
|
+
- 小规模数据(几十个元素)上,连续 `vector` 的线性扫描常比哈希表更快;不要机械套用复杂度。
|
|
23
|
+
- 递归深度可能随输入增长时,改成迭代或显式栈;栈溢出不是可捕获的异常。
|
|
24
|
+
|
|
25
|
+
## 内存与缓存
|
|
26
|
+
|
|
27
|
+
- 连续存储对缓存友好;`vector<struct>` 比 `vector<unique_ptr<struct>>` 更容易预取。
|
|
28
|
+
- 只遍历部分字段时,AoS(数组的结构)可能浪费带宽;数据量大且按列访问时考虑 SoA(结构的数组),但先用 profiler 证明。
|
|
29
|
+
- 小对象按值传递通常比堆分配快;不要为了“避免拷贝”给所有参数加指针。
|
|
30
|
+
- `reserve` 减少 `vector` 扩容和搬移;只有已知上界或能准确估算时才用。
|
|
31
|
+
- 移动大对象比拷贝便宜,但移动不是免费;不要把 `std::move` 当成优化开关。
|
|
32
|
+
- 返回局部对象直接 `return value;`,依赖 RVO / NRVO;`return std::move(value);` 会阻止优化。
|
|
33
|
+
|
|
34
|
+
## 未定义行为与安全
|
|
35
|
+
|
|
36
|
+
以下行为没有标准结果,优化后可能表现为“有时对”:
|
|
37
|
+
|
|
38
|
+
- 数组/容器越界访问(`operator[]`、指针算术)。
|
|
39
|
+
- 解引用空指针或已释放对象(use-after-free、double free)。
|
|
40
|
+
- 读取未初始化变量。
|
|
41
|
+
- 有符号整数溢出。
|
|
42
|
+
- 数据竞争:多个线程无同步地读写同一对象。
|
|
43
|
+
- 用 `reinterpret_cast` 把对象当无关类型访问,违反严格别名规则。
|
|
44
|
+
- 对象生存期结束后继续使用指针或引用。
|
|
45
|
+
|
|
46
|
+
工具:
|
|
47
|
+
|
|
48
|
+
- AddressSanitizer(ASan)查越界、use-after-free、double free。
|
|
49
|
+
- UndefinedBehaviorSanitizer(UBSan)查有符号溢出、空指针、对齐等问题。
|
|
50
|
+
- ThreadSanitizer(TSan)查数据竞争。
|
|
51
|
+
- `-Wall -Wextra -Wpedantic` 打开常用警告;不要靠关闭警告“修复”问题。
|
|
52
|
+
|
|
53
|
+
ASan/UBSan 有运行期开销,只在测试和排查时开;发布构建仍要保留编译器警告。
|
|
54
|
+
|
|
55
|
+
## 常见性能陷阱
|
|
56
|
+
|
|
57
|
+
| 现象 | 常见原因 | 先做什么 |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| 循环里反复分配 | 每次迭代构造容器/字符串 | 移到循环外,或 `reserve` |
|
|
60
|
+
| 大量拷贝 | 范围 `for` 按值、参数按值传大对象 | 改 `const&`,确认所有权 |
|
|
61
|
+
| 哈希表慢 | 哈希冲突、频繁 rehash | 预留桶数,检查哈希质量 |
|
|
62
|
+
| 字符串拼接慢 | 反复 `+` 产生临时对象 | `reserve` + `+=`,或 `std::format` 一次成型 |
|
|
63
|
+
| 多线程没有变快 | 锁竞争、伪共享 | 缩小临界区,按缓存行对齐 |
|
|
64
|
+
| 递归慢/栈溢出 | 重复子问题、深度大 | 记忆化或改迭代 |
|
|
65
|
+
| 输出慢 | 每行 flush、频繁 `std::endl` | 用 `'\n'`,需要时再 flush |
|
|
66
|
+
|
|
67
|
+
## 不要做的“优化”
|
|
68
|
+
|
|
69
|
+
- 不要用全局可变状态换速度;正确性和可测试性先于常数因子。
|
|
70
|
+
- 不要为未测量的热点写复杂的手工内存管理。
|
|
71
|
+
- 不要用 `reinterpret_cast` 绕过类型系统;需要二进制布局时用 `std::bit_cast` 并确认可平凡复制。
|
|
72
|
+
- 不要关闭整类 clang-tidy 检查来让构建通过;修代码或局部 `NOLINT` 并写理由。
|
|
73
|
+
- 不要把 `std::endl` 当换行符;它每次都会 flush。
|
|
74
|
+
|
|
75
|
+
## 检查清单
|
|
76
|
+
|
|
77
|
+
- [ ] 性能目标可测量,测量条件已记录。
|
|
78
|
+
- [ ] 先看复杂度,再看缓存,再看常数。
|
|
79
|
+
- [ ] 没有未测量的“优化”。
|
|
80
|
+
- [ ] 大对象传递和返回没有多余拷贝。
|
|
81
|
+
- [ ] ASan/UBSan 在测试中跑过,警告没有被关闭。
|
|
82
|
+
- [ ] 没有用未定义行为换速度。
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# 领域建模与项目设计
|
|
2
|
+
|
|
3
|
+
在写第一个类之前,先决定领域里有哪些值、哪些规则、哪些边界。类型设计错了,后面的命名、注释和测试都只能补救。
|
|
4
|
+
|
|
5
|
+
## 先找领域值
|
|
6
|
+
|
|
7
|
+
把需求里的名词分成四类:
|
|
8
|
+
|
|
9
|
+
| 类别 | 例子 | 表示 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| 数量、标识、尺寸 | 优先级、编号、长度 | 定宽整数或强类型包装 |
|
|
12
|
+
| 参与者、角色 | 用户、当前处理人 | `enum class`,取值封闭 |
|
|
13
|
+
| 状态、类别 | 待处理、进行中、已完成 | `enum class`,取值封闭 |
|
|
14
|
+
| 动作、结果 | 添加、完成、拒绝及原因 | `enum class` 或聚合 `struct` |
|
|
15
|
+
|
|
16
|
+
- 取值集合封闭时用 `enum class`,不用 `int` 常量或字符串。
|
|
17
|
+
- 有多个字段且总是一起传递的值用 `struct` 聚合;需要维护不变量的对象用 `class`。
|
|
18
|
+
- 同一底层类型承载不同含义(米/秒、行/列)且容易混用时,用轻量强类型包装。
|
|
19
|
+
- 不为每个名词造类;只有出现行为、不变量或独立变化原因时才升级为 `class`。
|
|
20
|
+
|
|
21
|
+
## 封闭状态与结果
|
|
22
|
+
|
|
23
|
+
- 状态用连续取值的 `enum class`:`kPending`、`kActive`、`kDone`。
|
|
24
|
+
- 状态转换写成显式函数或规则对象,不用一串 `if` 直接改整数。
|
|
25
|
+
- “动作是否合法”和“动作执行结果”分开:合法性查询返回 `bool` 或原因,执行返回结果对象。
|
|
26
|
+
- 可预期失败(编号不存在、输入格式不对)用返回值表达;违反合同(下标越界、状态机不可能分支)才抛异常。
|
|
27
|
+
- 结果对象携带足够信息让调用方决定下一步,不要只返回 `bool` 再让调用方猜原因。
|
|
28
|
+
|
|
29
|
+
## 规则核心与外围
|
|
30
|
+
|
|
31
|
+
规则核心只依赖标准库和领域类型:
|
|
32
|
+
|
|
33
|
+
- 不读终端、不写终端、不碰 GUI、Web、Python 或数据库。
|
|
34
|
+
- 不依赖当前时间、随机数种子、环境变量或工作目录;这些由外围注入。
|
|
35
|
+
- 输入是领域值,输出是领域结果;文本格式、JSON、HTTP 都在外围转换。
|
|
36
|
+
- 核心可以被单元测试直接调用,不需要启动进程或模拟终端。
|
|
37
|
+
|
|
38
|
+
外围按职责分层:
|
|
39
|
+
|
|
40
|
+
1. 规则核心:任务集合、状态转换、排序和过滤。
|
|
41
|
+
2. 应用层:把一次用户动作翻译成核心调用,把结果翻译成协议。
|
|
42
|
+
3. 适配层:终端、JSON 行、HTTP、Python 脚本、GUI。
|
|
43
|
+
4. 组装层:`main` 只解析参数、创建对象、连接输入输出。
|
|
44
|
+
|
|
45
|
+
依赖方向从外向内;核心不知道适配层存在。
|
|
46
|
+
|
|
47
|
+
## 边界的划分理由
|
|
48
|
+
|
|
49
|
+
按“变化原因”划分边界,不按文件数量:
|
|
50
|
+
|
|
51
|
+
- 规则变化(优先级规则、合法动作)和界面变化(提示文案、按钮布局)是两个原因,放两个组件。
|
|
52
|
+
- 协议变化(JSON 字段)和规则变化是两个原因,协议转换放应用/适配层。
|
|
53
|
+
- 平台变化(Windows/WSL/macOS 路径、动态库查找)放构建脚本或适配层,不进核心。
|
|
54
|
+
- 测试需要替换的外部依赖(时钟、随机源、输入输出)通过接口注入,不在核心内部直接调用。
|
|
55
|
+
|
|
56
|
+
公共头文件只暴露稳定契约;实现细节放 `.cpp` 或私有头。一个组件一个 `CMakeLists.txt`,组件的 `include/` 是对外接口,`src/` 是实现。
|
|
57
|
+
|
|
58
|
+
## 什么时候抽共享库
|
|
59
|
+
|
|
60
|
+
- 只有第二个真实使用方出现,且两边语义确实相同,才抽共享组件。
|
|
61
|
+
- 抽之前先复制,观察两处是否同步变化;如果变化方向不同,说明抽象错了。
|
|
62
|
+
- 共享库的公共接口要能独立编译、独立测试;不要为了“复用”把两个领域的类型塞进一个头文件。
|
|
63
|
+
- 抽象的理由写进项目技能或设计注释;没有理由的抽象比重复更难维护。
|
|
64
|
+
|
|
65
|
+
## 规则有歧义时
|
|
66
|
+
|
|
67
|
+
- 先列出至少两个合理解释,再问用户选哪一个;不要静默选一个继续写。
|
|
68
|
+
- 问题要具体到可观察行为:“当两个任务的优先级和截止日期都相同时,按添加顺序还是按名称排序?”
|
|
69
|
+
- 用户回答后,把决定写进项目技能的行为表或测试名;下次遇到同一歧义不再猜。
|
|
70
|
+
- 不能问时,选更保守的解释(不产生非法状态、不丢信息),并在交付说明里标注假设。
|
|
71
|
+
|
|
72
|
+
## 检查清单
|
|
73
|
+
|
|
74
|
+
- [ ] 领域值有明确类型,封闭集合用 `enum class`。
|
|
75
|
+
- [ ] 规则核心不依赖终端、GUI、Web、Python 或当前环境。
|
|
76
|
+
- [ ] 可预期失败用返回值,合同违反才抛异常。
|
|
77
|
+
- [ ] 边界按变化原因划分,依赖方向从外向内。
|
|
78
|
+
- [ ] 共享抽象有第二个真实使用方和明确理由。
|
|
79
|
+
- [ ] 规则歧义已问清或记录了保守假设。
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 标准库选择
|
|
2
|
+
|
|
3
|
+
标准库不是“能用就行”的工具箱。先确定访问模式、所有权和失败方式,再选容器和算法。
|
|
4
|
+
|
|
5
|
+
## 容器选择
|
|
6
|
+
|
|
7
|
+
| 需求 | 默认选择 | 说明 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 固定长度、编译期已知 | `std::array<T, N>` | 不堆分配,可当聚合成员;长度是类型的一部分 |
|
|
10
|
+
| 可变长度、连续存储 | `std::vector<T>` | 默认容器;随机访问、尾部追加、缓存友好 |
|
|
11
|
+
| 两端增删 | `std::deque<T>` | 分段连续;中间插入仍然昂贵 |
|
|
12
|
+
| 文本 | `std::string` | 拥有字符;修改后 `c_str()` / `data()` 可能失效 |
|
|
13
|
+
| 只读文本视图 | `std::string_view` | 不拥有字符;不能比原字符串活得更久 |
|
|
14
|
+
| 有序键值 | `std::map<K, V>` | 按 key 有序;查找 O(log n);节点独立 |
|
|
15
|
+
| 无序键值 | `std::unordered_map<K, V>` | 平均 O(1),最坏 O(n);迭代顺序不稳定 |
|
|
16
|
+
| 去重集合 | `std::set` / `std::unordered_set` | 只存 key;不要用 `map<K, bool>` 代替 |
|
|
17
|
+
| 连续只读视图 | `std::span<T>` | 不拥有元素;C++20 没有 `at` |
|
|
18
|
+
|
|
19
|
+
选择顺序:
|
|
20
|
+
|
|
21
|
+
1. 先问是否需要稳定顺序、是否需要按 key 查找、是否需要连续内存。
|
|
22
|
+
2. 默认 `vector`;只有剖析或明确复杂度要求时才换。
|
|
23
|
+
3. 需要“可能没有”的返回值时用 `std::optional<T>`,不要用特殊值(`-1`、空字符串)表示缺失。
|
|
24
|
+
4. 需要“成功值或错误原因”时用 `std::expected<T, E>`(C++23)——C++20 项目改用项目自己的结果类型或 `std::variant`,不要假称标准库已有。
|
|
25
|
+
5. 需要多态值集合时用 `std::variant` 或 `std::unique_ptr<Base>`;不要用 `void*`。
|
|
26
|
+
|
|
27
|
+
## 访问与边界
|
|
28
|
+
|
|
29
|
+
- `operator[]` 不检查边界,越界是未定义行为;`at` 检查并抛 `std::out_of_range`。规则与家族差异见 `references/types.md`。
|
|
30
|
+
- `vector::front()` / `back()` / `data()` 在空容器上未定义;先 `empty()`。
|
|
31
|
+
- `map::operator[]` 缺键会插入默认值;只读查询用 `find`、`contains`(C++20)或 `at`。
|
|
32
|
+
- `string_view` 不保证以 `\0` 结尾;传给需要 C 字符串的 API 前先转成 `std::string`。
|
|
33
|
+
- `span` 不拥有元素,也不延长生命周期;不要从临时容器构造后保存。
|
|
34
|
+
- 迭代器区间是半开的 `[first, last)`;`end()` 不指向元素,不能解引用。
|
|
35
|
+
|
|
36
|
+
## 迭代与失效
|
|
37
|
+
|
|
38
|
+
- 范围 `for` 默认按值复制元素;大对象写 `const auto&`,需要修改写 `auto&`。
|
|
39
|
+
- 在范围 `for` 里修改容器大小会使迭代器失效;先收集要改的 key,再统一修改。
|
|
40
|
+
- `vector` 扩容会使所有指针、引用、迭代器失效;`reserve` 只能减少扩容次数,不能保证永不失效。
|
|
41
|
+
- `map` / `unordered_map` 插入不会使其他元素的引用失效;`unordered_map` 的 rehash 会使迭代器失效。
|
|
42
|
+
- 删除元素用 `erase-remove` 惯用法或 C++20 `std::erase` / `std::erase_if`;不要在遍历中直接 `erase` 当前迭代器以外的元素。
|
|
43
|
+
|
|
44
|
+
```cpp
|
|
45
|
+
std::vector<int> values{1, 2, 3, 4, 5};
|
|
46
|
+
std::erase_if(values, [](int value) { return value % 2 == 0; });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 算法优先于手写循环
|
|
50
|
+
|
|
51
|
+
能用标准算法表达意图时,不写下标循环:
|
|
52
|
+
|
|
53
|
+
| 意图 | 算法 |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| 查找 | `std::find` / `std::find_if` |
|
|
56
|
+
| 计数 | `std::count` / `std::count_if` |
|
|
57
|
+
| 任一/全部 | `std::any_of` / `std::all_of` / `std::none_of` |
|
|
58
|
+
| 变换 | `std::transform` |
|
|
59
|
+
| 排序 | `std::sort` / `std::stable_sort` |
|
|
60
|
+
| 最值 | `std::min_element` / `std::max_element` |
|
|
61
|
+
| 累加 | `std::accumulate`(注意初始值类型) |
|
|
62
|
+
| 二分查找 | `std::lower_bound` / `std::upper_bound`(要求已排序) |
|
|
63
|
+
|
|
64
|
+
- 算法接收迭代器区间,不接收容器;调用时写 `values.begin(), values.end()` 或 C++20 `std::ranges::sort(values)`。
|
|
65
|
+
- 谓词只做判断,不改元素;需要修改时用 `transform`。
|
|
66
|
+
- 捕获列表只捕获需要的变量;默认 `[&]` 容易悬空,默认 `[=]` 容易复制大对象。
|
|
67
|
+
- 不在比较函数里写有副作用的逻辑;排序要求严格弱序,违反会导致未定义行为。
|
|
68
|
+
|
|
69
|
+
## `std::string` 与 `std::string_view`
|
|
70
|
+
|
|
71
|
+
- 函数只读文本时参数用 `std::string_view`;需要保存或修改时用 `std::string`。
|
|
72
|
+
- `string_view` 不拥有内存;不要返回指向局部 `std::string` 的 `string_view`,也不要把临时 `std::string` 转成 `string_view` 后保存。
|
|
73
|
+
- 拼接少量文本用 `std::string` 的 `+=` 或 `std::format`;大量拼接先 `reserve`。
|
|
74
|
+
- 查找子串用 `find`,找不到返回 `npos`;不要用 `-1` 比较。
|
|
75
|
+
- 数值与文本互转用 `std::to_string` / `std::from_chars` / `std::format`;`std::stoi` 会抛异常且不报告消耗长度。
|
|
76
|
+
|
|
77
|
+
## `std::format` 与输出
|
|
78
|
+
|
|
79
|
+
- 组合消息优先 `std::format`;它返回 `std::string`,不直接写流。
|
|
80
|
+
- 输出仍走 `std::cout` / `std::cerr` / 项目传入的 `std::ostream&`;C++20 没有 `std::print`。
|
|
81
|
+
- 格式串里的 `{}` 按顺序取参数;需要位置时写 `{0}`、`{1}`。
|
|
82
|
+
- 自定义类型要格式化时,特化 `std::formatter<T>`;不要在 `operator<<` 里塞业务格式。
|
|
83
|
+
- 浮点默认格式可能与预期不同;需要固定精度时写 `{:.2f}`,需要科学计数时写 `{:e}`。
|
|
84
|
+
- 异常消息、日志、用户界面文本都通过 `std::format` 组合,不手工拼接数字和字符串。
|
|
85
|
+
|
|
86
|
+
## 时间、随机与文件
|
|
87
|
+
|
|
88
|
+
- 时间用 `<chrono>` 的 `std::chrono::steady_clock` 测耗时,不用系统时钟做间隔。
|
|
89
|
+
- 随机数用 `<random>` 的引擎 + 分布;不要用 `rand()`,也不要每次调用重新播种。
|
|
90
|
+
- 文件用 `std::ifstream` / `std::ofstream`,构造后检查 `is_open()`;读写失败检查流状态,不假设成功。
|
|
91
|
+
- 路径用 `std::filesystem::path`;拼接用 `/` 运算符,不手工拼分隔符。
|
|
92
|
+
|
|
93
|
+
## 检查清单
|
|
94
|
+
|
|
95
|
+
- [ ] 容器选择有明确理由,默认是 `vector`。
|
|
96
|
+
- [ ] 访问前有边界证明,或使用 `at`。
|
|
97
|
+
- [ ] 没有保存 `string_view` / `span` 指向的临时对象。
|
|
98
|
+
- [ ] 修改容器大小前处理了迭代器失效。
|
|
99
|
+
- [ ] 能用算法表达时没有手写下标循环。
|
|
100
|
+
- [ ] 输出用 `std::format` 组合、流输出,没有 `std::print`。
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# 类型、数值与值语义
|
|
2
|
+
|
|
3
|
+
类型不是存储格式的同义词。先写清一个值允许什么、禁止什么,再选 `int`、`std::int32_t`、`enum class` 还是包装类型。
|
|
4
|
+
|
|
5
|
+
## 宽度、范围与 `std::size_t`
|
|
6
|
+
|
|
7
|
+
- C++ 只保证基础整数类型的最小范围,不保证 `int` 固定 32 位,也不保证 `long` 一定宽于 `int`。
|
|
8
|
+
- `sizeof(T)` 返回 `std::size_t`;`<limits>` 的 `std::numeric_limits<T>` 查询最大值、最小值和特殊值。
|
|
9
|
+
- 协议、文件格式或跨平台布局要求固定宽度时,用 `<cstdint>` 的 `std::int32_t`、`std::uint64_t`,并确认目标平台提供该类型。
|
|
10
|
+
- `char` 是否有符号由实现决定;存字符时不要把它当成固定范围的小整数。
|
|
11
|
+
- 整数字面量的类型由后缀和候选类型决定;宽度或符号属于接口时,写出目标类型并检查转换。
|
|
12
|
+
|
|
13
|
+
`std::size_t` 是无符号类型,适合对象大小和容器下标。把它和负的 `int` 比较会先转成无符号,结果与直觉相反。统一下标类型,或先验证非负再比较。
|
|
14
|
+
|
|
15
|
+
## 有符号、无符号与溢出
|
|
16
|
+
|
|
17
|
+
- 普通整数类型默认有符号;只有确有非负语义且理解边界时才用无符号。
|
|
18
|
+
- 无符号运算按模 `2^N` 回绕,这是有定义的行为;有符号整数溢出是未定义行为。
|
|
19
|
+
- 数组越界、空指针解引用、读取未初始化变量都属于未定义行为。编译器可以假设它们不发生,优化后的结果可能完全不符合直觉。
|
|
20
|
+
- 两边都是整数时,`/` 向零截断,`%` 给余数;`7 / 2` 是 `3`。整数除以零是未定义行为,不能与浮点的 `inf` / `nan` 混为一谈。
|
|
21
|
+
- 浮点转整数会截断小数;大整数转小整数可能丢高位或改变符号。先检查范围,再 `static_cast`。
|
|
22
|
+
- 禁止 C 风格强转 `(int)value`;它隐藏转换类别,`google-readability-casting` 会报。
|
|
23
|
+
|
|
24
|
+
```cpp
|
|
25
|
+
int CountWide(const std::vector<int>& values) {
|
|
26
|
+
int count = 0;
|
|
27
|
+
for (std::size_t index = 0; index < values.size(); ++index) {
|
|
28
|
+
if (values[index] > 0) ++count;
|
|
29
|
+
}
|
|
30
|
+
return count;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
循环下标与容器大小都用 `std::size_t`,避免混合比较。业务编号另按下面的定宽约定。
|
|
35
|
+
|
|
36
|
+
## 项目对整型角色的约定
|
|
37
|
+
|
|
38
|
+
| 角色 | 类型 | 理由 |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| 业务编号、尺寸、参与边界判定的下标 | `std::int32_t` | 能表达负值,避开 `size_t` 与负数比较的陷阱 |
|
|
41
|
+
| 容器长度、`std::array` 长度、由编号换算的索引 | `std::size_t` | 与 STL 和 NTTP 一致 |
|
|
42
|
+
| 封闭状态码的底层类型 | `std::uint8_t` | 只表达取值集合和布局,不参与算术 |
|
|
43
|
+
|
|
44
|
+
- 不用裸 `int` 承担跨平台协议或布局;它的宽度不可预测。
|
|
45
|
+
- 不用 `std::uint8_t` / `std::uint16_t` 当业务编号;负值无法表达,整数提升还会制造意外。
|
|
46
|
+
- 有符号乘法进入容器长度前先显式拓宽,否则会触发 `bugprone-implicit-widening-of-multiplication-result`:
|
|
47
|
+
|
|
48
|
+
```cpp
|
|
49
|
+
inline constexpr std::int32_t kBatchSize = 3;
|
|
50
|
+
inline constexpr std::int32_t kItemsPerBatch = 3;
|
|
51
|
+
inline constexpr std::size_t kItemCount =
|
|
52
|
+
static_cast<std::size_t>(kBatchSize) * static_cast<std::size_t>(kItemsPerBatch);
|
|
53
|
+
std::array<TaskState, kItemCount> items_{};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- 下标换算在同一函数里先 `static_cast<std::size_t>` 各操作数再乘加;不要为此关闭检查,也不要改业务编号的类型。
|
|
57
|
+
|
|
58
|
+
## 枚举类
|
|
59
|
+
|
|
60
|
+
`enum class` 定义封闭取值集合:
|
|
61
|
+
|
|
62
|
+
- 枚举值只在类型作用域内可见:`TaskState::kPending`,不写 `kPending`。
|
|
63
|
+
- 与整数之间不发生隐式转换;需要编号时写 `static_cast<int>(state)`,只用于输出或既有接口。
|
|
64
|
+
- 同类型值可以比较和赋值;不能参与算术。
|
|
65
|
+
- 需要固定布局时写底层类型:`enum class TaskState : std::uint8_t { kPending, kActive, kDone };`
|
|
66
|
+
|
|
67
|
+
约定:
|
|
68
|
+
|
|
69
|
+
- 新代码用 `enum class`;unscoped `enum` 只用于兼容 C 接口,不作为默认写法。
|
|
70
|
+
- 封闭状态码默认写 `: std::uint8_t`;普通枚举用默认底层类型。
|
|
71
|
+
- 枚举值的行尾注释要么全写要么全不写;语义结果写在最前,`///<` 对齐。
|
|
72
|
+
- 需要“编号”与“名字”两套表示时,写显式映射函数,例如 `TaskStateToName` / `TaskStateFromName`,不要在枚举和整数之间来回隐式转换。
|
|
73
|
+
|
|
74
|
+
## 强类型包装
|
|
75
|
+
|
|
76
|
+
同一底层类型承载不同领域含义时,轻量包装能把“不该混用”变成编译错误:
|
|
77
|
+
|
|
78
|
+
```cpp
|
|
79
|
+
struct Meters {
|
|
80
|
+
double value{0.0};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
struct Seconds {
|
|
84
|
+
double value{0.0};
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- 包装是聚合,成员公开,仍可构造不合理值;需要不变量时改用 `class` 并在构造函数检查。
|
|
89
|
+
- 取值封闭的领域值用 `enum class`,不要为了“强类型”再包一层。
|
|
90
|
+
- 包装的代价是取值要写 `.value`;只在确实存在混用风险时使用,不为每个 `int` 造类型。
|
|
91
|
+
|
|
92
|
+
## 成员函数尾部的 `const`
|
|
93
|
+
|
|
94
|
+
```cpp
|
|
95
|
+
class TaskBoard {
|
|
96
|
+
public:
|
|
97
|
+
[[nodiscard]] TaskState GetTaskState(TaskId id) const;
|
|
98
|
+
void SetTaskState(TaskId id, TaskState state);
|
|
99
|
+
|
|
100
|
+
private:
|
|
101
|
+
std::array<TaskState, kTaskCount> tasks_{};
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- 尾部 `const` 承诺不修改实例自身;与参数上的 `const`、返回类型上的 `const`、局部变量的 `const` 各管一段。
|
|
106
|
+
- 查询型成员一律标尾部 `const`;修改型成员不标,误标会被编译器拒绝。
|
|
107
|
+
- 返回成员容器的只读视图时,返回 `const` 引用或按值返回;不要返回可变内部引用。
|
|
108
|
+
- `mutable` 只用于缓存、互斥量等“逻辑 const”场景,并在注释里写理由;不要用它绕过正常的 const 设计。
|
|
109
|
+
|
|
110
|
+
## `[[nodiscard]]`
|
|
111
|
+
|
|
112
|
+
- 只给“结果携带信息、忽略会出错”的函数:查询型 `const` 成员与返回结果对象的 API。
|
|
113
|
+
- 不给所有非 `void` 函数机械添加;标注的价值来自稀缺。
|
|
114
|
+
- 有意丢弃时写 `static_cast<void>(result)`,不要为此全局关闭警告。
|
|
115
|
+
- 测试里验证“必须处理结果”时,同时验证忽略能被编译器发现。
|
|
116
|
+
|
|
117
|
+
## 下标与 `at`
|
|
118
|
+
|
|
119
|
+
同一容器上,`operator[]` 与 `at` 的失败方式不同:
|
|
120
|
+
|
|
121
|
+
| 族 | 容器 | `[]` 越界 | `at` 越界 |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| 按下标 | `array`、`vector`、`deque`、`string`、`string_view` | 未定义行为 | 抛 `std::out_of_range` |
|
|
124
|
+
| 按键 | `map`、`unordered_map` | 缺键则插入默认值 | 缺键则抛 `std::out_of_range` |
|
|
125
|
+
| 无下标 | `set`、`unordered_set`、`multimap` | 无 `operator[]` / `at` | — |
|
|
126
|
+
|
|
127
|
+
- 只在调用点已经证明下标合法时用 `[]`;否则用 `at` 或先做边界检查。
|
|
128
|
+
- `std::span` 在 C++20 没有 `at`;`span::at` 到 C++26 才进入标准,不要假称 C++20 可用。
|
|
129
|
+
- `map::operator[]` 会插入,不是“读取可能缺失的键”;只读查询用 `find` / `contains` / `at`。
|
|
130
|
+
- 契约失败的消息用 `std::format`,见 `references/exceptions.md`。
|
|
131
|
+
|
|
132
|
+
## 浮点与布尔
|
|
133
|
+
|
|
134
|
+
- 二进制浮点通常不能精确表示十进制小数,`0.1 + 0.2` 不要直接与 `0.3` 用 `==` 比较。
|
|
135
|
+
- 比较计算结果时使用与量纲、误差预算匹配的容差;单独处理 `NaN` 与无穷大。
|
|
136
|
+
- 金额存最小货币单位的整数(如“分”);折扣率、比例、物理量可用 `double`,但要写清精度边界。
|
|
137
|
+
- 整数 `0` 转 `false`,非 `0` 转 `true`;流默认输出 `0` / `1`,需要可读文本时先写 `std::boolalpha`。
|
|
138
|
+
- 不要把 `bool` 当成承载多个状态的整数;互斥状态用 `enum class`。
|
|
139
|
+
|
|
140
|
+
## 检查清单
|
|
141
|
+
|
|
142
|
+
- [ ] 参与协议、布局或跨平台边界的整数有明确宽度。
|
|
143
|
+
- [ ] 业务编号与容器下标没有混用有符号和无符号。
|
|
144
|
+
- [ ] 封闭状态集合用 `enum class`,没有裸整数状态码。
|
|
145
|
+
- [ ] 查询成员标了尾部 `const`;`[[nodiscard]]` 只出现在结果型 API。
|
|
146
|
+
- [ ] 下标访问前有边界证明,或使用 `at`。
|
|
147
|
+
- [ ] 浮点比较有容差,金额没有用 `double` 结算。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# 第 1 步:定行为
|
|
2
|
+
|
|
3
|
+
## 产出
|
|
4
|
+
|
|
5
|
+
一句可验收的行为描述,加上这一处的输入、输出和失败方式。行为句只写观察得到的结果,不写类名、容器或算法。
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
给定 <输入>,程序输出 <结果>;当 <非法输入> 时,输出 <错误> 并以 <方式> 结束。
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
有项目阶段表时,先找到这一阶段的行;行为句必须落在这一行范围内。没有阶段表时,只写用户在这次请求里点名的行为,不编造阶段。
|
|
12
|
+
|
|
13
|
+
## 通用情况
|
|
14
|
+
|
|
15
|
+
- 先写「用户能看到什么」:终端输出、文件内容、退出码、库函数返回值。内部状态不算可观察行为。
|
|
16
|
+
- 一次只定一个可验收的切片。切片太大就拆成两个行为句,分别走完四步。
|
|
17
|
+
- 失败方式也要写:非法参数、空输入、越界、文件不存在时,是返回错误值、打印提示,还是抛异常。
|
|
18
|
+
- 行为句不绑定实现。「用 `std::vector` 保存」是声明或实现的决定,不是行为。
|
|
19
|
+
- 需要新增输出时,组合消息用 `std::format`;不要为了打印引入 C++23 的 `std::print`。
|
|
20
|
+
|
|
21
|
+
## 情况
|
|
22
|
+
|
|
23
|
+
**新功能**:先写最小可观察切片,例如「输入名字,输出一句问候」。不要一次把交互、存档、排行榜都写进第 1 步。
|
|
24
|
+
|
|
25
|
+
**领域规则**:先把动词和名词写成领域值——任务、状态、动作、结果。取值封闭时用 `enum class`;规则有歧义时列出至少两个解释并问用户,不静默选一个。领域值怎么选见 `references/project-design.md`。
|
|
26
|
+
|
|
27
|
+
**修缺陷**:行为句写成「现在观察到 X,期望观察到 Y」,并保留复现输入。修完只验证 X → Y,不顺手改无关行为。
|
|
28
|
+
|
|
29
|
+
**重构**:行为句写成「输入输出不变」,并列出必须保持的边界(退出码、错误文本、文件格式)。重构不得改变可观察行为。
|
|
30
|
+
|
|
31
|
+
**没有阶段表**:只写用户点名的行为;用户没说失败方式时,按现有程序惯例补一句,并在完成时说明这是补充假设。
|
|
32
|
+
|
|
33
|
+
## 正反例指针
|
|
34
|
+
|
|
35
|
+
- 边界怎么切、库和程序怎么分:`examples/structure.md`「行为与边界」。
|
|
36
|
+
- 行为句写得太大或混入实现:`examples/structure.md`「行为句」。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 第 2 步:定边界
|
|
2
|
+
|
|
3
|
+
## 产出
|
|
4
|
+
|
|
5
|
+
一张边界表:每个组件放什么、哪些头文件是公共接口、库 target 依赖什么、程序 target 依赖什么。还要写清终端输入输出放在哪里。
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
<component>/include/<name>.h 公共接口
|
|
9
|
+
<component>/src/<name>.cpp 实现
|
|
10
|
+
tests/ 测试
|
|
11
|
+
app 只做参数、输入输出和装配
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 通用情况
|
|
15
|
+
|
|
16
|
+
- 库 target 不依赖终端、GUI、网络脚本或全局单例;它只接收值、返回值、抛合同异常。
|
|
17
|
+
- 程序 target 链接库,负责读参数、读标准输入、打印结果、把退出码翻译成进程退出码。
|
|
18
|
+
- 公共头文件直接放在组件的 `include/` 根下,用裸文件名包含;不要写 `#include "<component>/x.h"`。
|
|
19
|
+
- 依赖方向从程序指向库。库不反向包含程序的 `cli.h`,也不引用 `main.cpp`。
|
|
20
|
+
- 测试放 `tests/`,测试 target 链接被测库;测试不复制库的实现。
|
|
21
|
+
- 只有一个可执行文件、没有复用需求时,可以不分库;一旦出现第二个调用方或测试需要直接调用,再拆库。
|
|
22
|
+
- 抽共享库前先确认第二个真实使用方和相同语义;只是“看起来会复用”不构成理由。抽象理由写进项目技能或设计注释。
|
|
23
|
+
- 第 2 步不写 `CMakeLists.txt`。只决定边界,构建 target 留给 `building-cpp` 的定构建。
|
|
24
|
+
|
|
25
|
+
## 情况
|
|
26
|
+
|
|
27
|
+
**单文件程序**:`src/main.cpp` 可以有 `main()` 和私有辅助函数;一旦辅助逻辑需要测试,把它移到库组件。
|
|
28
|
+
|
|
29
|
+
**领域核心 + 外围**:规则核心(状态、合法性、结果)只依赖标准和领域类型;终端、GUI、Web、脚本都在适配层。依赖方向从外向内,核心不知道适配层存在。
|
|
30
|
+
|
|
31
|
+
**库 + 程序**:库放在 `<component>/`,程序放在 `app/` 或根 `src/main.cpp`;程序只包含库的公共头。公共头只暴露稳定契约,实现细节放 `.cpp` 或私有头。
|
|
32
|
+
|
|
33
|
+
**静态库 / 动态库**:接口相同,差别在链接方式和运行时库路径;都遵守「库不依赖终端」。选哪种由 `building-cpp` 的定构建决定。
|
|
34
|
+
|
|
35
|
+
**测试**:测试通过公共头调用库,不包含 `src/` 私有头;需要测私有逻辑时,先判断它是否应该成为公共行为。
|
|
36
|
+
|
|
37
|
+
## 正反例指针
|
|
38
|
+
|
|
39
|
+
- 终端输出被塞进库、程序反向依赖:`examples/structure.md`「边界」。
|
|
40
|
+
- 公共头放在 `src/`、用带组件前缀的包含路径:`examples/structure.md`「头文件位置」。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 第 3 步:定声明
|
|
2
|
+
|
|
3
|
+
## 产出
|
|
4
|
+
|
|
5
|
+
公共头文件、类型与函数声明、命名、声明顺序和声明注释。声明要能让调用方在不读 `.cpp` 的情况下知道怎么用、什么时候失败。
|
|
6
|
+
|
|
7
|
+
## 通用情况
|
|
8
|
+
|
|
9
|
+
- 语言固定 C++20,不使用 C++23 特性;头文件用 `#pragma once`。
|
|
10
|
+
- 命名按 `references/naming.md`:类型与函数 PascalCase,变量与参数 snake_case,类成员尾随 `_`,常量与枚举子 `k` 前缀。
|
|
11
|
+
- 声明顺序按 `references/declaration-order.md`:类型 → 静态常量 → 工厂 → 构造/赋值/析构 → 其他函数 → 数据成员。
|
|
12
|
+
- 公共头文件自包含:自身需要的头都直接包含,不依赖调用方的包含顺序。
|
|
13
|
+
- 单参数构造函数写 `explicit`;可查询的成员函数加 `[[nodiscard]]`;默认成员初始化写在声明处,不在构造函数里重复。
|
|
14
|
+
- 可预期的失败用返回值表达(结果类型、`bool` + 输出参数、`std::optional`);合同违反才在声明注释里写清抛出的异常类型。
|
|
15
|
+
- 声明注释写契约:参数范围、返回值、失败方式、所有权。不要写实现步骤。
|
|
16
|
+
- `std::format` 用于组合消息;声明里不要承诺 `std::print`(C++23)。
|
|
17
|
+
|
|
18
|
+
## 情况
|
|
19
|
+
|
|
20
|
+
**新增类型**:先判断是聚合 `struct`、有不变量的 `class`,还是 `enum class`;接口多于数据且有 invariant 时用 `class`,纯数据传递用 `struct`。
|
|
21
|
+
|
|
22
|
+
**改公共接口**:先找所有调用方;改签名、改返回类型、改异常合同都算破坏性变更。能加默认参数或重载时优先加,不要静默改变旧调用的含义。
|
|
23
|
+
|
|
24
|
+
**异常合同**:构造函数、`Get` 类查询、解析函数最容易违反合同;在声明注释里写「当……时抛出 `std::invalid_argument` / `std::out_of_range`」,不要只写「可能抛异常」。
|
|
25
|
+
|
|
26
|
+
**注释**:声明注释用 `///`,只写调用方需要知道的;实现细节留给 `.cpp`。文件头、章节横幅和 TODO 的格式见 `references/comments.md`。
|
|
27
|
+
|
|
28
|
+
## 正反例指针
|
|
29
|
+
|
|
30
|
+
- 命名、访问器、语义角色:`examples/naming.md`。
|
|
31
|
+
- 异常合同与返回值选择:`examples/exceptions.md`。
|
|
32
|
+
- 声明注释写成了实现说明:`examples/comments.md`「声明注释」。
|