@chengzhao-dev/dsh-cpp-workflow 0.3.1 → 0.5.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.
Files changed (38) hide show
  1. package/README.en.md +10 -9
  2. package/README.md +10 -9
  3. package/lib/index.js +4 -4
  4. package/locale/en.json +1 -1
  5. package/locale/zh.json +1 -1
  6. package/package.json +2 -2
  7. package/skills/building-cpp/SKILL.md +32 -11
  8. package/skills/building-cpp/references/build-script.md +11 -0
  9. package/skills/building-cpp/references/linking.md +1 -1
  10. package/skills/building-cpp/references/standards/README.md +1 -1
  11. package/skills/building-cpp/steps/01-build.md +22 -35
  12. package/skills/configuring-cmake/SKILL.md +65 -0
  13. package/skills/configuring-cmake/assets/cmake/CompileSettings.cmake +126 -0
  14. package/skills/configuring-cmake/assets/cmake/LinkSettings.cmake +48 -0
  15. package/skills/{building-cpp → configuring-cmake}/examples/cmake.md +84 -10
  16. package/skills/{building-cpp → configuring-cmake}/references/cmake.md +49 -43
  17. package/skills/configuring-cmake/references/cxx-standard.md +67 -0
  18. package/skills/configuring-cmake/references/modules.md +105 -0
  19. package/skills/configuring-cmake/steps/01-targets.md +53 -0
  20. package/skills/configuring-cmake/steps/02-modules.md +37 -0
  21. package/skills/configuring-cmake/steps/03-check.md +39 -0
  22. package/skills/developing-cpp/SKILL.md +9 -1
  23. package/skills/developing-cpp/examples/{comments.md → comments-declaration.md} +25 -51
  24. package/skills/developing-cpp/examples/comments-implementation.md +53 -0
  25. package/skills/developing-cpp/examples/exceptions.md +38 -19
  26. package/skills/developing-cpp/examples/structure.md +71 -0
  27. package/skills/developing-cpp/references/comments.md +49 -137
  28. package/skills/developing-cpp/references/exceptions.md +12 -13
  29. package/skills/developing-cpp/references/language.md +1 -0
  30. package/skills/developing-cpp/references/naming.md +2 -2
  31. package/skills/developing-cpp/references/standard-library.md +1 -0
  32. package/skills/developing-cpp/references/types.md +1 -2
  33. package/skills/developing-cpp/scripts/checking-comments.mjs +57 -2
  34. package/skills/developing-cpp/steps/03-declaration.md +4 -4
  35. package/skills/developing-cpp/steps/04-implementation.md +5 -4
  36. package/skills/building-cpp/assets/cmake/applying-warnings.cmake +0 -10
  37. package/skills/building-cpp/assets/cmake/fetching-googletest.cmake +0 -15
  38. package/skills/building-cpp/assets/cmake/setting-rpath.cmake +0 -8
@@ -0,0 +1,53 @@
1
+ # 第 1 步:定 target 与标准
2
+
3
+ ## 产出
4
+
5
+ 一张明确的 target 图、一组 target 属性、一个与 `environment.json` 一致的语言标准。构建结果只取决于声明的组合,不取决于当前 shell 的默认值。
6
+
7
+ ## 通用情况
8
+
9
+ 先读 `environment.json` 的 `standard`,再把它映射成两层:
10
+
11
+ | `standard` | `target_compile_features` | `CXX_STANDARD` | CMake 最低版本 |
12
+ | --- | --- | --- | --- |
13
+ | `c++11` | `cxx_std_11` | `11` | 3.1 |
14
+ | `c++14` | `cxx_std_14` | `14` | 3.1 |
15
+ | `c++17` | `cxx_std_17` | `17` | 3.8 |
16
+ | `c++20` | `cxx_std_20` | `20` | 3.12 |
17
+ | `c++23` | `cxx_std_23` | `23` | 3.20 |
18
+
19
+ target 规则:
20
+
21
+ - 程序 target 固定叫 `app`;库 target 用组件名,程序链接库;不把 `main()` 编进库。
22
+ - 每个 target 自己声明 include、标准、编译选项和链接库;公共头用 `PUBLIC`,实现目录用 `PRIVATE`。
23
+ - 用 `target_include_directories`、`target_compile_features`、`target_link_libraries`;禁止全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS`。
24
+ - 源文件显式列出;不写 `file(GLOB ...)`,新增文件时改 CMake。
25
+ - `set(CMAKE_EXPORT_COMPILE_COMMANDS ON)`;可执行文件输出到 `build/<compiler>/bin`,库到 `build/<compiler>/lib`。
26
+ - 需要直接运行构建产物的共享库程序,设 `BUILD_RPATH "$ORIGIN/../lib"`;Apple 用 `@loader_path/../lib`。
27
+ - 标准用项目 `cmake/` 里的 `CompileSettings_SetCxxStandard(target visibility c++NN)`;没有这个函数时先从 `assets/cmake/CompileSettings.cmake` 复制并导入,不要在每个 target 重复三属性。
28
+ - 警告用同一份 `CompileSettings.cmake` 里的 `CompileSettings_SetWarnings(target visibility LEVEL basic|standard|strict)`;`visibility` 用 `PRIVATE` 或 `INTERFACE`,不要在每个 target 重复 `-Wall` 列表。
29
+
30
+ ## 命令形状
31
+
32
+ ```bash
33
+ cmake -S . -B build/clang -G Ninja \
34
+ -DCMAKE_BUILD_TYPE=Debug \
35
+ -DCMAKE_CXX_COMPILER=clang++ \
36
+ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
37
+ ```
38
+
39
+ GCC 模式把目录和编译器换成 `build/gcc`、`g++`。一个 configure 只用一个编译器;CMake 最低 3.20,低于目标标准所需版本时先升 `cmake_minimum_required`,不要静默降标准。
40
+
41
+ ## 情况
42
+
43
+ **新工程**:工程模板仍由 `building-cpp` 提供,从 `../../building-cpp/assets/templates/projects/` 选形态,只复制目标工程缺的文件;模板里的 `{{PROJECT_NAME}}` 换成真实工程名,再按本节改 target 与标准。
44
+
45
+ **已有工程**:先读现有 `CMakeLists.txt`,找出全局设置、GLOB、输出目录和 target 命名;只改与当前任务相关的一处。大改前先让现有构建跑通,保留可回退点。
46
+
47
+ **库 + 程序**:库的公共 include 用 `PUBLIC` 传播;程序不直接包含库的 `src/`。静态库与动态库的 target 写法相同,动态库额外设运行时路径。
48
+
49
+ **测试**:确有第二个消费者时再加 `enable_testing()` 与 `add_subdirectory(tests)`;测试 target 只链接被测库的公共接口,没有测试时不加空测试。
50
+
51
+ **预设**:使用 preset 时名称固定 `clang` / `gcc`,与构建目录一致;没有 preset 时直接用上面的命令,不要两套并存。
52
+
53
+ 下一步:target 图稳定后,进 `steps/02-modules.md` 决定哪些片段抽成函数。
@@ -0,0 +1,37 @@
1
+ # 第 2 步:定模块与函数
2
+
3
+ ## 产出
4
+
5
+ 一个可以重复导入的 `cmake/` 模块,或一份明确不抽函数的理由。模块只定义函数,目标目录不再复制同一段配置。
6
+
7
+ ## 什么时候抽
8
+
9
+ - 同一段 CMake 在至少两个 target 或两个子目录重复出现,且语义已经稳定。
10
+ - 标准、警告、RPATH 这类横切规则需要一处升级、全仓生效。
11
+ - 逻辑有独立失败路径,需要 `message(FATAL_ERROR)` 把非法输入挡在 configure 阶段。
12
+
13
+ 一次性三行属性、仍在频繁变化的实验片段、只服务一个 target 的私有设置不抽;等第二次真实复用再抽。
14
+
15
+ ## 文件与命名
16
+
17
+ - 一个文件放一组语义相近的函数,不为每个函数建单独文件;文件过大或出现无关职责时再拆。
18
+ - 文件名用 UpperCamelCase,按职责命名,例如 `CompileSettings.cmake`、`LinkSettings.cmake`。
19
+ - 函数名用 `<ModuleName>_` 加 PascalCase,例如 `CompileSettings_SetCxxStandard`;前缀取模块文件名的原样大小写,复制到工程后不改名。
20
+ - 工程自己新增、不准备回流的函数才用项目前缀加 snake_case。
21
+ - 模块开头写 `include_guard(GLOBAL)`,避免重复导入时重定义函数。
22
+
23
+ ## 导入
24
+
25
+ 根 `CMakeLists.txt` 在 `project()` 之后、`add_subdirectory()` 之前追加一次 `CMAKE_MODULE_PATH`,再 `include()` 需要的模块。子目录只调用函数,不再次导入。
26
+
27
+ ## 契约
28
+
29
+ - 公开函数上方写中文 `#` 契约:一句功能、空行、边界,再用 `:param` / `:keyword` 标参数;不写 `#[[.rst:` 与 `.. command::`。
30
+ - 完整写法见 `references/cmake.md`;签名留在 `function()` 那一行。
31
+ - 签名、警告等级与错误消息以 `assets/cmake/` 和 `references/modules.md` 为准;本步不复制第二份。
32
+ - 普通函数只改传入 target 的属性,不写全局变量、不读环境变量、不下载依赖;唯一例外是 `LinkSettings_FetchGoogleTest`,只在测试需要时调用。
33
+ - 先校验 target、可见性与标准,再调用 `target_*` 或 `set_target_properties`;失败用 `FATAL_ERROR`,消息带函数名与非法值。
34
+
35
+ ## 退出
36
+
37
+ 模块可被根文件导入且函数签名稳定后,进 `steps/03-check.md` 跑 configure、构建与数据库核对。排版、2 空格与注释形态按 `references/cmake.md`。
@@ -0,0 +1,39 @@
1
+ # 第 3 步:通过检查
2
+
3
+ ## 产出
4
+
5
+ 一次干净的 configure、一次构建、一份与 `environment.json` 一致的编译数据库。检查只回答「target 契约是否成立」,完整的格式化、静态检查、CTest 与运行由 `building-cpp` 的通过检查接手。
6
+
7
+ ## 命令
8
+
9
+ ```bash
10
+ cmake -S . -B build/clang -G Ninja \
11
+ -DCMAKE_BUILD_TYPE=Debug \
12
+ -DCMAKE_CXX_COMPILER=clang++ \
13
+ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
14
+ cmake --build build/clang
15
+ ```
16
+
17
+ GCC 模式把目录和编译器换成 `build/gcc`、`g++`。每次检查都重新 configure,不依赖上一次缓存;同一构建目录只放同一编译器的配置。
18
+
19
+ ## 核对项
20
+
21
+ - **target 图**:`app` 链接库,库的公共 include 用 `PUBLIC`;没有把 `main()` 编进库。
22
+ - **标准**:读 `build/clang/compile_commands.json` 的最后一个 `-std=`,必须与 `environment.json` 的 `standard` 一致,不能是 `gnu++NN` 或更高默认标准。
23
+ - **没有全局污染**:全仓搜索 `include_directories`、`add_definitions`、`CMAKE_CXX_FLAGS`、`file(GLOB`,命中就回第 1 步。
24
+ - **模块导入**:根 `CMakeLists.txt` 只导入一次;函数名与文件名可追溯;删除旧模块后没有残留 `include()`。
25
+ - **排版**:`CMakeLists.txt` 与 `*.cmake` 无 tab、2 空格缩进、UTF-8 无 BOM、LF;抽查 80 列以上的行是否有必要。
26
+ - **产物位置**:可执行文件在 `build/<compiler>/bin`,库在 `build/<compiler>/lib`;源码目录没有新增构建产物。
27
+
28
+ ## 失败分流
29
+
30
+ | 现象 | 回哪一步 |
31
+ | --- | --- |
32
+ | 找不到 target、链接失败、include 路径不对 | 第 1 步 |
33
+ | 目标标准没有生效、`-std` 与声明不一致 | 第 1 步,必要时重读 `references/cxx-standard.md` |
34
+ | 函数参数没校验、模块重复导入、旧模块残留 | 第 2 步 |
35
+ | 只是 clang-format、clang-tidy、CTest 或运行失败 | 转 `building-cpp` 的通过检查 |
36
+
37
+ ## 边界
38
+
39
+ 本步不替代 `building-cpp`:不安装工具链,不改 `running-build.sh`,不跑 `clang-tidy` 或 CTest。检查通过的判据是 configure 与构建成功、数据库标准正确、结构规则无命中;完整的工具链检查仍由 `building-cpp` 的通过检查给出。
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  ## 适用场景
11
11
 
12
- 管 `.h` / `.hpp` / `.cpp` / `.cc` 里换一台机器也不会改的内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文采用 C++ Core Guidelines 的 Error handling,作为开发阶段的源码指导;它不进入插件运行时。`CMakeLists.txt`、`cmake/`、`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 只开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `building-cpp` 的定构建,把文件写进 CMake,再决定是否定工具配置,最后通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
12
+ 管 `.h` / `.hpp` / `.cpp` / `.cc` 里换一台机器也不会改的内容:可观察行为、库与程序的边界、声明、命名、异常和源码注释。文件槽位与守卫以 `references/file-template.md` 为准;排版、命名、头文件与包含顺序以 `references/google-cpp-style.md` 的 Google 基线为准。异常条文采用 C++ Core Guidelines 的 Error handling,作为开发阶段的源码指导;它不进入插件运行时。`CMakeLists.txt` 与 `cmake/` 开 `configuring-cmake`;`running-build.sh`、`.clang-*`、`.vscode/` 和 `environment.json` 开 `building-cpp`。只改函数体、命名、异常或注释且入口命令可运行时,定实现后只开 `building-cpp` 的「通过检查」;新增或移动源文件、改库与程序的边界、补测试 target 时,声明和实现可以先写,但完成前必须开 `configuring-cmake` 把文件写进 CMake,再开 `building-cpp` 定工具配置并通过检查。`.qmd`、仓库 README、Python 和非 C++ 文档不打开本技能。
13
13
 
14
14
  存在项目技能 `cpp-workflow-project` 时先读它的 `SKILL.md`:工程名、库与程序的边界、阶段表、术语增补、入口命令。没有项目技能时门禁不会放出本技能,不要猜项目约定。
15
15
 
@@ -35,6 +35,14 @@ metadata:
35
35
 
36
36
  每步只打开同号的一个步骤文件。判不准才打开 `examples/` 里与该步对应的案例;`references/` 保存完整条文,只在排错、整目录重构,或步骤里明确指向某一条文时按需打开。性能问题读 `references/performance.md`,标准库设施选型读 `references/standard-library.md`,链接与产物的细节读 `building-cpp` 的 `references/linking.md`。
37
37
 
38
+ 本轮激活按三条上限走:
39
+
40
+ - 只开当前步对应的一个步骤文件;本轮已经开了别的技能,就不再开第二个技能的步骤。
41
+ - 条文只开该步点名的一份;同号案例留到下一次激活。
42
+ - 标准档只打开项目技能声明的那一档,没有声明时用本技能已有的新工程默认,不把其余标准档读进来。
43
+
44
+ 项目技能列出了检查命令时只跑那一条,没有时跑本技能自己的 `scripts/checking-comments.mjs`;退出码为 0 后在同一轮结束 goal,不再继续走没有失败的步骤。
45
+
38
46
  ### 四类改动
39
47
 
40
48
  先认这次改的是哪一类,再决定从哪一步进;一次改动混了两类时,按「写作 → 布局 → 排版 → 润色」的次序拆开做,前一类落定并经检查再进下一类。
@@ -1,4 +1,4 @@
1
- # 正反例:注释
1
+ # 正反例:声明注释与文件头
2
2
 
3
3
  ## 文件头
4
4
 
@@ -84,6 +84,30 @@ class Board {
84
84
 
85
85
  命名与风格约定写进项目文档和读者正文,注释里不解释「为什么用这种命名」。
86
86
 
87
+ ## 第一句:先写结果或限制
88
+
89
+ **差**:
90
+
91
+ ```cpp
92
+ /// 检查坐标。
93
+ /// 返回格子类型;越界会抛异常。
94
+ CellType GetCellType(CellPosition position) const;
95
+ ```
96
+
97
+ 前两句都没有直接回答“合法时拿到什么、越界时发生什么”,读者要读完三句才拼出合同。
98
+
99
+ **好**:
100
+
101
+ ```cpp
102
+ /// 坐标越界时抛出 std::out_of_range;合法坐标返回该格类型。
103
+ ///
104
+ /// @param position 行列坐标。
105
+ /// @throws std::out_of_range 坐标不在棋盘范围内。
106
+ CellType GetCellType(CellPosition position) const;
107
+ ```
108
+
109
+ 第一句就是结果或失败方式;后面的标签只补参数与异常细节,不再重复这句话。
110
+
87
111
  ## 函数契约
88
112
 
89
113
  **差**:
@@ -180,56 +204,6 @@ GameResult result_ = GameResult::kInProgress; // 对局结果;落子被拒时
180
204
 
181
205
  行尾写「存什么 + 初值含义 + 什么时候变」,同类成员对齐。
182
206
 
183
- ## 实现注释
184
-
185
- **差**:
186
-
187
- ```cpp
188
- // 遍历所有空格
189
- for (const auto& position : empty_cells) {
190
- // 尝试落子
191
- const bool wins = game.Board().CompletesLine(position, cell_type);
192
- // 如果能赢
193
- if (wins) {
194
- // 返回这一手
195
- return position;
196
- }
197
- }
198
- ```
199
-
200
- 每行都在复述代码。
201
-
202
- **好**:
203
-
204
- ```cpp
205
- // 先找能立刻赢的格子:启发式只在「马上赢」和「马上挡」之间做选择,
206
- // 不在这里做更深的前瞻,深搜留给 MinimaxAi。
207
- for (const auto& position : empty_cells) {
208
- if (game.CompletesLine(position, CellType::kX)) {
209
- return position;
210
- }
211
- }
212
- ```
213
-
214
- 解释「为什么停在这一层」,不翻译语句。
215
-
216
- ## TODO
217
-
218
- **差**:
219
-
220
- ```cpp
221
- // TODO: 参考 xxx
222
- // FIXME: 这里可能有问题
223
- ```
224
-
225
- **好**:
226
-
227
- ```cpp
228
- // TODO: 阶段 5 - 给 MinimaxAi 加置换表,减少重复局面的搜索。
229
- ```
230
-
231
- 有定位、有动作、能搜索。
232
-
233
207
  ## 配置注释
234
208
 
235
209
  **差**:
@@ -0,0 +1,53 @@
1
+ # 正反例:实现注释
2
+
3
+ 实现注释只写本层为什么停在这里;声明注释、文件头与配置注释见 `comments-declaration.md`。
4
+
5
+ ## 实现注释
6
+
7
+ **差**:
8
+
9
+ ```cpp
10
+ // 遍历所有空格
11
+ for (const auto& position : empty_cells) {
12
+ // 尝试落子
13
+ const bool wins = game.Board().CompletesLine(position, cell_type);
14
+ // 如果能赢
15
+ if (wins) {
16
+ // 返回这一手
17
+ return position;
18
+ }
19
+ }
20
+ ```
21
+
22
+ 每行都在复述代码。
23
+
24
+ **好**:
25
+
26
+ ```cpp
27
+ // 先找能立刻赢的格子:启发式只在「马上赢」和「马上挡」之间做选择,
28
+ // 不在这里做更深的前瞻,深搜留给 MinimaxAi。
29
+ for (const auto& position : empty_cells) {
30
+ if (game.CompletesLine(position, CellType::kX)) {
31
+ return position;
32
+ }
33
+ }
34
+ ```
35
+
36
+ 解释「为什么停在这一层」,不翻译语句。
37
+
38
+ ## TODO
39
+
40
+ **差**:
41
+
42
+ ```cpp
43
+ // TODO: 参考 xxx
44
+ // FIXME: 这里可能有问题
45
+ ```
46
+
47
+ **好**:
48
+
49
+ ```cpp
50
+ // TODO: 阶段 5 - 给 MinimaxAi 加置换表,减少重复局面的搜索。
51
+ ```
52
+
53
+ 有定位、有动作、能搜索。
@@ -22,11 +22,11 @@ struct ParseResult {
22
22
 
23
23
  [[nodiscard]] ParseResult TryParseTask(std::string_view line);
24
24
 
25
- /// @throws TaskIndexError index 不在 [0, size()) 内。
25
+ /// @throws std::out_of_range index 不在 [0, size()) 内。
26
26
  [[nodiscard]] Task GetTask(int index) const;
27
27
  ```
28
28
 
29
- `TryParseTask` 把失败作为正常结果返回。`GetTask` 的前提是调用方给出有效下标;下标无效时函数无法返回一个任务,于是抛出带领域语义的 `TaskIndexError`。如果越界其实是正常输入,应该改成返回 `std::optional<Task>`,而不是让调用方捕获异常。
29
+ `TryParseTask` 把失败作为正常结果返回。`GetTask` 的前提是调用方给出有效下标;越界时函数无法返回一个任务,抛标准库已有的 `std::out_of_range`。如果越界其实是正常输入,应该改成返回 `std::optional<Task>`,而不是让调用方捕获异常;只有项目需要把这种越界和别处的 `out_of_range` 区分开时,才定义派生类。
30
30
 
31
31
  ## 异常类型与消息
32
32
 
@@ -36,34 +36,53 @@ struct ParseResult {
36
36
  throw "bad index";
37
37
  ```
38
38
 
39
- **好**:定义有语义的类型;确实没有更多信息时,才直接用标准库通用类型。
39
+ **好**:语义已经由标准类型给出时直接用标准类型;需要额外携带领域信息时,再派生自它。
40
40
 
41
41
  ```cpp
42
- class TaskIndexError : public std::out_of_range {
43
- public:
44
- explicit TaskIndexError(const std::string& message)
45
- : std::out_of_range(message) {}
46
- };
47
-
48
- throw TaskIndexError{
42
+ throw std::out_of_range{
49
43
  std::format("GetTask: index={} 超出 [0, {})", index, tasks_.size())};
50
44
  ```
51
45
 
52
- 消息写操作和关键值,不写「出错了」;`std::format` 只是构造消息的方式,不强制固定模板。
46
+ 消息写操作和关键值,不写「出错了」;`std::format` 只是构造消息的方式,不强制固定模板。项目若定义 `Error` 基类,按 `references/exceptions.md` 先定基类再派生,调用方捕获基类接住整族。
47
+
48
+ ## Debug 与 Release 的三层合同
49
+
50
+ **差**:同一个函数在 Debug 抛异常、在 Release 返回 false;调用方被迫按构建模式写两套分支。
51
+
52
+ ```cpp
53
+ #ifdef NDEBUG
54
+ return false;
55
+ #else
56
+ throw std::out_of_range{row, col};
57
+ #endif
58
+ ```
59
+
60
+ **好**:返回值在两种模式完全一致;需要异常时两种模式都抛,只有 `what()` 的详略不同。
53
61
 
54
- ## 多语义类型:Boost.Exception 风格
62
+ ```cpp
63
+ // 返回值:失败是可恢复结果,两种模式都返回 false。
64
+ [[nodiscard]] bool TryPlace(CellPosition position, Player player) noexcept;
65
+
66
+ // 越界:两种模式都抛 std::out_of_range;Debug 消息可带 file:line。
67
+ void Place(CellPosition position, Player player);
68
+ ```
69
+
70
+ 越界用标准库已有的 `std::out_of_range`,调用方不需要认识项目私有类型;只有调用方必须把它和其它 `out_of_range` 区分开时,才定义派生类并在 `what()` 里补位置信息。
71
+
72
+ **差**:用 `assert` 承担 Release 也必须执行的 invariant 检查;`NDEBUG` 后检查消失。
73
+
74
+ ```cpp
75
+ assert(game.GetResult() == GameResult::kInProgress);
76
+ ```
55
77
 
56
- 一个错误可以同时属于多个类别。类型只做 `catch` 的标签,数据由基类或错误信息容器携带;多继承时用 virtual 基类,避免 `catch (const std::exception&)` 二义性。
78
+ **好**:两种模式都必须检查的 invariant 用 `Check`(项目保留时),失败时打印诊断并终止;只在开发期值得做、Release 可以省掉的自检用 `DebugCheck`。没有这两个宏时分别用 `assert` 与 `std::terminate`。
57
79
 
58
80
  ```cpp
59
- struct exception_base : virtual std::exception, virtual boost::exception {};
60
- struct io_error : virtual exception_base {};
61
- struct file_error : virtual io_error {};
62
- struct read_error : virtual io_error {};
63
- struct file_read_error : virtual file_error, virtual read_error {};
81
+ Check(game.GetResult() == GameResult::kInProgress, "Ai::Choose: game is already over");
82
+ DebugCheck(index < history_.size(), "history index out of range");
64
83
  ```
65
84
 
66
- 这只是设计典范,不要求引入 Boost。项目自己的异常层级也可以照这个形状写。
85
+ `Check` 不是 `[[noreturn]]`:条件成立时正常返回。`DebugCheck` 在 `NDEBUG` 后展开为空,参数不能带副作用。
67
86
 
68
87
  ## 捕获
69
88
 
@@ -80,3 +80,74 @@ task_list/src/detail.h // 私有头,只被本组件包含
80
80
  **该拆**:测试需要直接调用排序逻辑;或第二个程序要复用同一逻辑;或逻辑已经超过一个屏幕。
81
81
 
82
82
  拆的时候先定公共头(只暴露 `SortedByPriority`),把实现移到 `src/`,行为句不变,测试先绿再改构建。
83
+
84
+ ## 固定坐标用表
85
+
86
+ **差**:每次判断成线,都用循环把八条线的坐标重新拼一遍;行、列、对角线混在一个循环里,读者要跟着跑两遍才知道哪八条。
87
+
88
+ ```cpp
89
+ // 差:用循环现写八条线,坐标藏在控制流里。
90
+ std::vector<std::vector<CellPosition>> BuildLines() {
91
+ std::vector<std::vector<CellPosition>> lines;
92
+ for (int row = 0; row < kRows; ++row) { /* ... */ }
93
+ // 列与两条对角线再各写一段
94
+ return lines;
95
+ }
96
+ ```
97
+
98
+ **好**:固定坐标写成匿名命名空间里的常量表,`HasLine` 与 `IsLineComplete` 共用;格子用 `[]`,整条线 `all_of`、八条线 `any_of`。
99
+
100
+ ```cpp
101
+ // board.cpp 的匿名命名空间:行、列、对角线三组,坐标固定。
102
+ constexpr std::array<std::array<CellPosition, 3>, 8> kLines{{
103
+ {{{0, 0}, {0, 1}, {0, 2}}}, // 行 0
104
+ {{{1, 0}, {1, 1}, {1, 2}}}, // 行 1
105
+ {{{2, 0}, {2, 1}, {2, 2}}}, // 行 2
106
+ {{{0, 0}, {1, 0}, {2, 0}}}, // 列 0
107
+ {{{0, 1}, {1, 1}, {2, 1}}}, // 列 1
108
+ {{{0, 2}, {1, 2}, {2, 2}}}, // 列 2
109
+ {{{0, 0}, {1, 1}, {2, 2}}}, // 对角线
110
+ {{{0, 2}, {1, 1}, {2, 0}}}, // 反对角线
111
+ }};
112
+ ```
113
+
114
+ 每个格子要分别处理时仍写 `row` / `col` 循环,例如收集空格、终端打印。
115
+
116
+ ## 推演一步读原数据
117
+
118
+ **差**:先复制整盘,再在副本上落子,最后判断成线;调用方看不出副本只是中间量。
119
+
120
+ ```cpp
121
+ Board hypothetical = *this;
122
+ hypothetical.PlaceCell(position, cell_type);
123
+ return hypothetical.HasLine(cell_type);
124
+ ```
125
+
126
+ **好**:判断「这一格换成某种棋子后会不会成线」不需要第二份棋盘。越界、空格、已占用仍按 `PlaceCell` 的拒绝条件返回 `false`;通过之后只在包含该坐标的线上把这一格算作 `cell_type`,其余格子读原棋盘。
127
+
128
+ ```cpp
129
+ bool Board::IsLineComplete(CellPosition position, CellType cell_type) const noexcept {
130
+ if (!IsInRange(position) || cell_type == CellType::kEmpty) {
131
+ return false;
132
+ }
133
+ if (cells_[position.row][position.col] != CellType::kEmpty) {
134
+ return false;
135
+ }
136
+
137
+ const auto matches = [&](CellPosition cell) {
138
+ if (cell.row == position.row && cell.col == position.col) {
139
+ return true;
140
+ }
141
+ return cells_[cell.row][cell.col] == cell_type;
142
+ };
143
+
144
+ return std::ranges::any_of(kLines, [&](const auto& line) {
145
+ const bool contains = std::ranges::find_if(line, [&](CellPosition cell) {
146
+ return cell.row == position.row && cell.col == position.col;
147
+ }) != line.end();
148
+ return contains && std::ranges::all_of(line, matches);
149
+ });
150
+ }
151
+ ```
152
+
153
+ 只用 `[]` 与 `IsInRange`,不调用 `at()`,头文件上的 `noexcept` 可以留下;原棋盘不被写入。