@chengzhao-dev/dsh-cpp-workflow 0.3.0 → 0.5.0

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 (64) hide show
  1. package/README.en.md +14 -11
  2. package/README.md +14 -11
  3. package/lib/index.js +4 -4
  4. package/lib/workspace.js +1 -56
  5. package/locale/en.json +1 -1
  6. package/locale/zh.json +1 -1
  7. package/package.json +2 -2
  8. package/skills/building-cpp/SKILL.md +25 -12
  9. package/skills/building-cpp/assets/config/.clang-tidy +1 -0
  10. package/skills/building-cpp/assets/templates/projects/multi/CMakeLists.txt +2 -2
  11. package/skills/building-cpp/assets/templates/projects/shared-library/CMakeLists.txt +1 -1
  12. package/skills/building-cpp/assets/templates/projects/shared-library/greeting/CMakeLists.txt +1 -1
  13. package/skills/building-cpp/assets/templates/projects/single/CMakeLists.txt +1 -1
  14. package/skills/building-cpp/assets/templates/projects/static-library/CMakeLists.txt +1 -1
  15. package/skills/building-cpp/assets/templates/projects/static-library/greeting/CMakeLists.txt +1 -1
  16. package/skills/building-cpp/examples/tooling.md +3 -3
  17. package/skills/building-cpp/references/build-script.md +11 -0
  18. package/skills/building-cpp/references/linking.md +7 -7
  19. package/skills/building-cpp/references/standards/README.md +1 -1
  20. package/skills/building-cpp/references/static-analysis.md +3 -3
  21. package/skills/building-cpp/steps/01-build.md +22 -35
  22. package/skills/building-cpp/steps/02-tooling.md +2 -2
  23. package/skills/configuring-cmake/SKILL.md +57 -0
  24. package/skills/configuring-cmake/assets/cmake/CompileSettings.cmake +126 -0
  25. package/skills/configuring-cmake/assets/cmake/LinkSettings.cmake +48 -0
  26. package/skills/configuring-cmake/examples/cmake.md +224 -0
  27. package/skills/{building-cpp → configuring-cmake}/references/cmake.md +54 -48
  28. package/skills/configuring-cmake/references/cxx-standard.md +67 -0
  29. package/skills/configuring-cmake/references/modules.md +105 -0
  30. package/skills/configuring-cmake/steps/01-targets.md +53 -0
  31. package/skills/configuring-cmake/steps/02-modules.md +37 -0
  32. package/skills/configuring-cmake/steps/03-check.md +39 -0
  33. package/skills/developing-cpp/SKILL.md +17 -3
  34. package/skills/developing-cpp/examples/comments.md +173 -136
  35. package/skills/developing-cpp/examples/exceptions.md +38 -19
  36. package/skills/developing-cpp/examples/structure.md +75 -4
  37. package/skills/developing-cpp/references/comments.md +82 -99
  38. package/skills/developing-cpp/references/declaration-order.md +4 -2
  39. package/skills/developing-cpp/references/exceptions.md +12 -13
  40. package/skills/developing-cpp/references/file-template.md +131 -0
  41. package/skills/developing-cpp/references/glossary.md +3 -3
  42. package/skills/developing-cpp/references/google-cpp-style.md +8 -8
  43. package/skills/developing-cpp/references/language.md +4 -3
  44. package/skills/developing-cpp/references/layout.md +21 -12
  45. package/skills/developing-cpp/references/naming.md +5 -4
  46. package/skills/developing-cpp/references/project-design.md +1 -1
  47. package/skills/developing-cpp/references/standard-library.md +2 -1
  48. package/skills/developing-cpp/references/types.md +1 -2
  49. package/skills/developing-cpp/scripts/checking-comments.mjs +371 -0
  50. package/skills/developing-cpp/steps/01-behavior.md +1 -1
  51. package/skills/developing-cpp/steps/02-boundary.md +4 -4
  52. package/skills/developing-cpp/steps/03-declaration.md +7 -7
  53. package/skills/developing-cpp/steps/04-implementation.md +6 -5
  54. package/skills/building-cpp/assets/cmake/applying-warnings.cmake +0 -10
  55. package/skills/building-cpp/assets/cmake/fetching-googletest.cmake +0 -15
  56. package/skills/building-cpp/assets/cmake/setting-rpath.cmake +0 -8
  57. package/skills/building-cpp/examples/cmake.md +0 -150
  58. /package/skills/building-cpp/assets/templates/projects/multi/src/{greeting.cc → greeting.cpp} +0 -0
  59. /package/skills/building-cpp/assets/templates/projects/multi/src/{main.cc → main.cpp} +0 -0
  60. /package/skills/building-cpp/assets/templates/projects/shared-library/greeting/src/{greeting.cc → greeting.cpp} +0 -0
  61. /package/skills/building-cpp/assets/templates/projects/shared-library/src/{main.cc → main.cpp} +0 -0
  62. /package/skills/building-cpp/assets/templates/projects/single/src/{main.cc → main.cpp} +0 -0
  63. /package/skills/building-cpp/assets/templates/projects/static-library/greeting/src/{greeting.cc → greeting.cpp} +0 -0
  64. /package/skills/building-cpp/assets/templates/projects/static-library/src/{main.cc → main.cpp} +0 -0
@@ -5,258 +5,295 @@
5
5
  **差**:
6
6
 
7
7
  ```cpp
8
- // task_list.cc
9
- // 作者:张三
10
- // 创建:2024-01-01
11
- // 修改:2024-02-01 修复 bug
8
+ // board.h
9
+ // 作者:chengzhao-dev
10
+ // 创建:2026-01-01
11
+ // 修改:2026-03-02 增加 CompletesLine
12
12
  ```
13
13
 
14
14
  **好**:
15
15
 
16
16
  ```cpp
17
17
  //===----------------------------------------------------------------------===//
18
- // TaskList 的存储、优先级排序与稳定顺序。
18
+ // board.h - 定义井字棋棋盘。
19
19
  //
20
- // 不处理终端输入输出;程序层负责解析「优先级 名称」并打印。
20
+ // 棋盘只保存格子状态与规则判断,不含对局流程;对局流程在 game.h。
21
21
  //===----------------------------------------------------------------------===//
22
22
  ```
23
23
 
24
- 文件头回答「这个文件负责什么、不负责什么」,不写作者和变更史。
24
+ 文件头回答「这个文件负责什么、不负责什么」,不写作者和变更史。后两行是这条边界:读的人由此知道该去哪个文件找回合推进。
25
25
 
26
- ## 声明注释
26
+ ## 类型注释:概念与约定,不是功能清单
27
27
 
28
28
  **差**:
29
29
 
30
30
  ```cpp
31
- /// 获取任务。
32
- /// @param index 索引
33
- /// @return 任务
34
- Task GetTask(int index) const;
31
+ /// 井字棋棋盘。
32
+ ///
33
+ /// 提供检查坐标区间、检查棋盘格子是否为空、获取棋盘格子类型、放置棋子、清空棋盘和判定终局的功能。
34
+ /// 棋盘为 3 行 3 列。
35
+ class Board {
35
36
  ```
36
37
 
37
- 问题:没说越界行为,没说返回的是拷贝还是引用。
38
+ 问题有两处:把成员函数抄了一遍,接口一改就过期;「棋盘为 3 行 3 列」被拆成单独一句,读者不知道它在回答什么问题。
38
39
 
39
- **好**:
40
+ **差**(把别的类型和以后才发生的事都写进来):
40
41
 
41
42
  ```cpp
42
- /// 返回 index 处的任务副本。
43
+ /// 井字棋棋盘。
43
44
  ///
44
- /// @param index 任务下标,必须在 [0, tasks_.size()) 内
45
- /// @return 该位置的任务
46
- /// @throws TaskIndexError index 越界
47
- [[nodiscard]] Task GetTask(int index) const;
45
+ /// 棋盘只保存 3×3 的格子状态和规则判断,不含回合推进:谁该落子、落子被拒后怎么提示留在 Game。
46
+ ///
47
+ /// 棋盘是值语义,可以自由拷贝;所有查询都是 const,对局和搜索可以交替读取同一棋盘。
48
+ class Board {
48
49
  ```
49
50
 
50
- 契约完整:范围、返回、异常声明。
51
+ 第二段说的是 `Game` 负责什么,读者读完仍不知道 `Board` 保证什么,重构一次这句话就过期;后两段里的可拷贝、`const` 查询在类声明里已经写清,「搜索要读同一棋盘」还不是现在的调用方。读者真正需要的是这个类型自己的不变量与失败方式。
51
52
 
52
- ## 实现注释
53
-
54
- **差**:
53
+ **好**:
55
54
 
56
55
  ```cpp
57
- // 如果 tasks_ 为空
58
- if (tasks_.empty()) {
59
- // 返回空
60
- return {};
61
- }
62
- // 遍历 tasks_
63
- for (const auto& task : tasks_) {
64
- // 比较优先级
65
- if (task.priority < best.priority) {
66
- // 更新 best
67
- best = task;
68
- }
69
- }
56
+ /// 井字棋棋盘。
57
+ ///
58
+ /// 3×3 个格子,每格取空、X 或 O;越界坐标不会写入棋盘。
59
+ class Board {
70
60
  ```
71
61
 
72
- 每行都在复述代码。
62
+ 两句都是本类型能兑现、读代码又不容易一眼看全的事,函数各自的契约下沉到函数注释;设计理由与分层不写进注释,写进书稿。
73
63
 
74
- **好**:
64
+ ## 风格理由不进注释
65
+
66
+ **差**(在十几个函数上各写一遍):
75
67
 
76
68
  ```cpp
77
- // 空列表是合法状态,调用方不应被迫先判空。
78
- if (tasks_.empty()) {
79
- return {};
80
- }
69
+ /// 检查坐标是否在范围内。函数名用 UpperCamel 而不是 snake_case,与标准库区分。
70
+ [[nodiscard]] static bool IsInRange(CellPosition position) noexcept;
81
71
 
82
- // 优先级相同时保留先加入的任务:用严格小于,不交换等值元素。
83
- for (const auto& task : tasks_) {
84
- if (task.priority < best.priority) {
85
- best = task;
86
- }
87
- }
72
+ /// 检查格子是否为空。函数名用 UpperCamel 而不是 snake_case,与标准库区分。
73
+ [[nodiscard]] bool IsCellEmpty(CellPosition position) const;
88
74
  ```
89
75
 
90
- 解释「为什么」和「稳定排序的取舍」,不翻译语句。
76
+ **好**(只写契约):
91
77
 
92
- ## TODO
78
+ ```cpp
79
+ /// 检查坐标是否落在棋盘下标区间内。
80
+ ///
81
+ /// 只判断几何边界,不检查格子是否为空、对局是否已经结束。
82
+ [[nodiscard]] static bool IsInRange(CellPosition position) noexcept;
83
+ ```
84
+
85
+ 命名与风格约定写进项目文档和读者正文,注释里不解释「为什么用这种命名」。
86
+
87
+ ## 第一句:先写结果或限制
93
88
 
94
89
  **差**:
95
90
 
96
91
  ```cpp
97
- // TODO: 参考 xxx
98
- // FIXME: 这里可能有问题
99
- // TODO(张三): 优化
92
+ /// 检查坐标。
93
+ /// 返回格子类型;越界会抛异常。
94
+ CellType GetCellType(CellPosition position) const;
100
95
  ```
101
96
 
97
+ 前两句都没有直接回答“合法时拿到什么、越界时发生什么”,读者要读完三句才拼出合同。
98
+
102
99
  **好**:
103
100
 
104
101
  ```cpp
105
- // TODO: 阶段 2 - 支持按截止日期排序
106
- // TODO: issue #42 - 把优先级解析抽成 TryParsePriority
102
+ /// 坐标越界时抛出 std::out_of_range;合法坐标返回该格类型。
103
+ ///
104
+ /// @param position 行列坐标。
105
+ /// @throws std::out_of_range 坐标不在棋盘范围内。
106
+ CellType GetCellType(CellPosition position) const;
107
107
  ```
108
108
 
109
- 有定位、有动作、能搜索。
109
+ 第一句就是结果或失败方式;后面的标签只补参数与异常细节,不再重复这句话。
110
110
 
111
- ## 配置注释
111
+ ## 函数契约
112
112
 
113
113
  **差**:
114
114
 
115
- ```yaml
116
- # 这是 clang-format 配置
117
- # 详见某文档
118
- ColumnLimit: 100
115
+ ```cpp
116
+ /// 尝试执行一次落子。
117
+ ///
118
+ /// @param move 落子。
119
+ /// @returns 落子执行成功返回 kPlaced,被拒绝返回 kRejected。
120
+ [[nodiscard]] PlaceResult TryPlace(const Move& move);
119
121
  ```
120
122
 
123
+ `@param move 落子` 是把类型名翻译成中文,没有信息;`@returns` 不是 Doxygen 标签;没有说被拒绝的条件,调用方只能读实现。
124
+
121
125
  **好**:
122
126
 
123
- ```yaml
124
- # 列宽、缩进、包含顺序都取 Google 预设,不逐项覆盖。
125
- Language: Cpp
126
- BasedOnStyle: Google
127
- Standard: Latest
127
+ ```cpp
128
+ /// 尝试执行一次落子。
129
+ ///
130
+ /// 只有本局仍在进行、且落子玩家正是当前玩家时才继续,棋盘、轮次、历史三者一起更新。
131
+ ///
132
+ /// @param move 待执行的落子,玩家必须是当前轮到的玩家。
133
+ /// @return 落子写入棋盘并换手时返回 kPlaced;对局已结束或上述任一条件不满足时返回 kRejected,
134
+ /// 棋盘和轮次都不变。
135
+ [[nodiscard]] PlaceResult TryPlace(const Move& move);
128
136
  ```
129
137
 
130
- 解释为什么选预设,不写策略头,不指文档路径。
131
-
132
- ## 横幅
138
+ ## 布尔查询要写「不检查什么」
133
139
 
134
140
  **差**:
135
141
 
136
142
  ```cpp
137
- // ===== 函数 =====
138
- void AddTask();
139
- void RemoveTask();
140
- // ===== 结束 =====
143
+ /// 检查指定坐标上的棋盘格子是否为空。
144
+ [[nodiscard]] bool IsCellEmpty(CellPosition position) const;
141
145
  ```
142
146
 
143
147
  **好**:
144
148
 
145
149
  ```cpp
146
- //===----------------------------------------------------------------------===//
147
- // 查询
148
- //===----------------------------------------------------------------------===//
149
-
150
- [[nodiscard]] std::size_t Size() const;
151
- [[nodiscard]] bool IsEmpty() const;
150
+ /// 检查指定坐标上的棋盘格子是否为空。
151
+ ///
152
+ /// 只回答「这一格能不能落子」,不判断对局是否已经结束:对局结束后格子仍是空的,
153
+ /// 但再落子没有意义。是否需要先看对局状态由调用方决定,棋盘不替它下结论。
154
+ ///
155
+ /// @param position 待检查的坐标。
156
+ /// @return 坐标在棋盘内且该格为空时返回 true;越界或该格已有棋子时返回 false。
157
+ [[nodiscard]] bool IsCellEmpty(CellPosition position) const;
152
158
  ```
153
159
 
154
- 横幅是跳转锚点,不是装饰;一组函数一个横幅,单函数不加。
160
+ 布尔函数最容易被误用;把「不检查什么」写出来,比多写一个参数说明有用。
155
161
 
156
- ## 枚举行尾注释
162
+ ## 枚举
157
163
 
158
164
  **差**:
159
165
 
160
166
  ```cpp
161
- enum class TaskStatus : std::uint8_t {
162
- kPending, // 待处理
163
- kActive, // 处理中
164
- kBlocked, // 等待外部输入
165
- kDone // 已完成
167
+ enum class Player : std::uint8_t {
168
+ kCross, // X
169
+ kNought // O
166
170
  };
167
171
  ```
168
172
 
169
- 问题:四个成员都写等于没写,且把名字重复了一遍。
170
-
171
173
  **好**:
172
174
 
173
175
  ```cpp
174
- enum class TaskStatus : std::uint8_t {
175
- kPending, ///< 已登记,尚未开始
176
- kActive, ///< 正在处理
177
- kBlocked, ///< 等待外部输入,不能继续
178
- kDone ///< 已完成,不再改动
176
+ /// 对局结果。
177
+ enum class GameResult : std::uint8_t {
178
+ kInProgress = 0, ///< 游戏进行中:还可以落子。
179
+ kCrossWin = 1, ///< X 获胜:达成三连。
180
+ kNoughtWin = 2, ///< O 获胜:达成三连。
181
+ kDraw = 3, ///< 平局:棋盘已满,双方都没有三连。
179
182
  };
180
183
  ```
181
184
 
182
- 若只有部分成员需要解释,同一枚举内要写就写全,或全不写;不要半套。语义结果写在最前,判定条件放冒号后。
185
+ 行尾注释先写语义,需要时用冒号补一句能核实的条件或结果,两段都短;不重复名字,不写嵌套子句,同一枚举要么全写要么全不写。
183
186
 
184
- ## 声明注释的完整契约
187
+ ## 数据成员
185
188
 
186
189
  **差**:
187
190
 
188
191
  ```cpp
189
- /// 修改任务优先级。
190
- /// @param id 任务编号
191
- /// @param priority 优先级
192
- void SetPriority(TaskId id, int priority);
192
+ Board board_;
193
+ Player current_player_;
194
+ GameResult result_;
193
195
  ```
194
196
 
195
197
  **好**:
196
198
 
197
199
  ```cpp
198
- /// 修改任务优先级。
199
- ///
200
- /// @param id 任务编号,必须已存在
201
- /// @param priority 新优先级,必须在 [0, 9] 内
202
- /// @throws TaskIndexError 编号不在当前任务表内
203
- /// @throws InvalidPriorityError priority 越界
204
- void SetPriority(TaskId id, int priority);
200
+ Board board_; // 对局棋盘。
201
+ Player current_player_ = Player::kCross; // 当前轮到的玩家,开局时 X 先手。
202
+ GameResult result_ = GameResult::kInProgress; // 对局结果;落子被拒时不改写。
205
203
  ```
206
204
 
207
- 只写调用者无法从签名推出来的内容:范围、所有权、失败方式。`@brief` 不写,第一行就是摘要。
205
+ 行尾写「存什么 + 初值含义 + 什么时候变」,同类成员对齐。
208
206
 
209
- ## 相邻注释
207
+ ## 实现注释
210
208
 
211
209
  **差**:
212
210
 
213
211
  ```cpp
214
- // 这里先检查存在性
215
- // 然后检查状态
216
- // 最后写入
217
- if (!Contains(id)) return false;
218
- if (tasks_[Index(id)].status == TaskStatus::kDone) return false;
219
- tasks_[Index(id)].status = TaskStatus::kActive;
220
- return true;
212
+ // 遍历所有空格
213
+ for (const auto& position : empty_cells) {
214
+ // 尝试落子
215
+ const bool wins = game.Board().CompletesLine(position, cell_type);
216
+ // 如果能赢
217
+ if (wins) {
218
+ // 返回这一手
219
+ return position;
220
+ }
221
+ }
221
222
  ```
222
223
 
224
+ 每行都在复述代码。
225
+
223
226
  **好**:
224
227
 
225
228
  ```cpp
226
- // 先检查再写入:失败时不留下半完成状态。
227
- if (!Contains(id)) return false;
228
- if (tasks_[Index(id)].status == TaskStatus::kDone) return false;
229
- tasks_[Index(id)].status = TaskStatus::kActive;
230
- return true;
229
+ // 先找能立刻赢的格子:启发式只在「马上赢」和「马上挡」之间做选择,
230
+ // 不在这里做更深的前瞻,深搜留给 MinimaxAi。
231
+ for (const auto& position : empty_cells) {
232
+ if (game.CompletesLine(position, CellType::kX)) {
233
+ return position;
234
+ }
235
+ }
231
236
  ```
232
237
 
233
- 一段连续的实现注释只讲一个判断或一个取舍;不是逐行字幕。
238
+ 解释「为什么停在这一层」,不翻译语句。
234
239
 
235
- ## 成员还是同一命名空间的函数
240
+ ## TODO
236
241
 
237
242
  **差**:
238
243
 
239
244
  ```cpp
240
- class TaskTable {
241
- public:
242
- std::size_t CountOverdue() const;
243
- bool HasOverdue() const { return CountOverdue() > 0; }
244
- };
245
+ // TODO: 参考 xxx
246
+ // FIXME: 这里可能有问题
245
247
  ```
246
248
 
247
- `HasOverdue` 只组合公开查询,不需要直接访问表示,却做成了成员。
249
+ **好**:
250
+
251
+ ```cpp
252
+ // TODO: 阶段 5 - 给 MinimaxAi 加置换表,减少重复局面的搜索。
253
+ ```
254
+
255
+ 有定位、有动作、能搜索。
256
+
257
+ ## 配置注释
258
+
259
+ **差**:
260
+
261
+ ```yaml
262
+ # 这是 clang-format 配置
263
+ # 详见某文档
264
+ ColumnLimit: 100
265
+ ```
248
266
 
249
267
  **好**:
250
268
 
269
+ ```yaml
270
+ # 列宽、缩进、包含顺序都取 Google 预设,不逐项覆盖。
271
+ Language: Cpp
272
+ BasedOnStyle: Google
273
+ Standard: Latest
274
+ ```
275
+
276
+ 解释为什么选预设,不写策略头,不指文档路径。
277
+
278
+ ## 不该进注释的东西
279
+
280
+ **差**:
281
+
251
282
  ```cpp
252
- class TaskTable {
253
- public:
254
- std::size_t CountOverdue() const;
255
- };
283
+ // 2026-03-02 把 Board 从 Game 里拆出来,因为之前 Game 太长。
284
+ // 参见 https://example.com/2026/03/02/refactor
285
+ // 其余成员见下方注释,两处不重复。
286
+ // 本团队要求所有类注释写满五行。
287
+ ```
256
288
 
257
- bool HasOverdue(const TaskTable& table) {
258
- return table.CountOverdue() > 0;
259
- }
289
+ **好**:
290
+
291
+ ```cpp
292
+ //===----------------------------------------------------------------------===//
293
+ // board.h - 棋盘状态与规则判断
294
+ //
295
+ // 这一层只有格子状态和胜负判定;回合推进在 game.h,输入输出在 apps/。
296
+ //===----------------------------------------------------------------------===//
260
297
  ```
261
298
 
262
- 函数和类放在同一命名空间;调用方只看到 `HasOverdue(table)`,类不增加只为转发而存在的成员。
299
+ 变更史进提交说明,外部资料进文档,团队流程进规范文件,跨文件的边界写文件头;类注释只写本类型自己的契约。
@@ -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
 
@@ -20,7 +20,7 @@
20
20
  **差**:库函数直接打印,程序只调用它。
21
21
 
22
22
  ```cpp
23
- // task_list.cc(库)
23
+ // task_list.cpp(库)
24
24
  void TaskList::Print() const {
25
25
  for (const auto& task : tasks_) {
26
26
  std::cout << task.priority << " " << task.name << "\n";
@@ -36,7 +36,7 @@ void TaskList::Print() const {
36
36
  // task_list.h(库)
37
37
  [[nodiscard]] std::vector<Task> SortedByPriority() const;
38
38
 
39
- // app/cli.cc(程序)
39
+ // app/cli.cpp(程序)
40
40
  void PrintTasks(const std::vector<Task>& tasks, std::ostream& out) {
41
41
  for (const auto& task : tasks) {
42
42
  out << std::format("{} {}\n", task.priority, task.name);
@@ -63,7 +63,7 @@ src/detail.h // 私有头被测试直接包含
63
63
 
64
64
  ```text
65
65
  task_list/include/task_list.h // 公共头
66
- task_list/src/task_list.cc // 实现
66
+ task_list/src/task_list.cpp // 实现
67
67
  task_list/src/detail.h // 私有头,只被本组件包含
68
68
  ```
69
69
 
@@ -75,8 +75,79 @@ task_list/src/detail.h // 私有头,只被本组件包含
75
75
 
76
76
  ## 单文件何时拆库
77
77
 
78
- **先不拆**:一个 `main.cc`,辅助函数在匿名命名空间,只有程序自己用。
78
+ **先不拆**:一个 `main.cpp`,辅助函数在匿名命名空间,只有程序自己用。
79
79
 
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` 可以留下;原棋盘不被写入。