@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.
- package/README.en.md +14 -11
- package/README.md +14 -11
- package/lib/index.js +4 -4
- package/lib/workspace.js +1 -56
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/skills/building-cpp/SKILL.md +25 -12
- package/skills/building-cpp/assets/config/.clang-tidy +1 -0
- package/skills/building-cpp/assets/templates/projects/multi/CMakeLists.txt +2 -2
- package/skills/building-cpp/assets/templates/projects/shared-library/CMakeLists.txt +1 -1
- package/skills/building-cpp/assets/templates/projects/shared-library/greeting/CMakeLists.txt +1 -1
- package/skills/building-cpp/assets/templates/projects/single/CMakeLists.txt +1 -1
- package/skills/building-cpp/assets/templates/projects/static-library/CMakeLists.txt +1 -1
- package/skills/building-cpp/assets/templates/projects/static-library/greeting/CMakeLists.txt +1 -1
- package/skills/building-cpp/examples/tooling.md +3 -3
- package/skills/building-cpp/references/build-script.md +11 -0
- package/skills/building-cpp/references/linking.md +7 -7
- package/skills/building-cpp/references/standards/README.md +1 -1
- package/skills/building-cpp/references/static-analysis.md +3 -3
- package/skills/building-cpp/steps/01-build.md +22 -35
- package/skills/building-cpp/steps/02-tooling.md +2 -2
- package/skills/configuring-cmake/SKILL.md +57 -0
- package/skills/configuring-cmake/assets/cmake/CompileSettings.cmake +126 -0
- package/skills/configuring-cmake/assets/cmake/LinkSettings.cmake +48 -0
- package/skills/configuring-cmake/examples/cmake.md +224 -0
- package/skills/{building-cpp → configuring-cmake}/references/cmake.md +54 -48
- package/skills/configuring-cmake/references/cxx-standard.md +67 -0
- package/skills/configuring-cmake/references/modules.md +105 -0
- package/skills/configuring-cmake/steps/01-targets.md +53 -0
- package/skills/configuring-cmake/steps/02-modules.md +37 -0
- package/skills/configuring-cmake/steps/03-check.md +39 -0
- package/skills/developing-cpp/SKILL.md +17 -3
- package/skills/developing-cpp/examples/comments.md +173 -136
- package/skills/developing-cpp/examples/exceptions.md +38 -19
- package/skills/developing-cpp/examples/structure.md +75 -4
- package/skills/developing-cpp/references/comments.md +82 -99
- package/skills/developing-cpp/references/declaration-order.md +4 -2
- package/skills/developing-cpp/references/exceptions.md +12 -13
- package/skills/developing-cpp/references/file-template.md +131 -0
- package/skills/developing-cpp/references/glossary.md +3 -3
- package/skills/developing-cpp/references/google-cpp-style.md +8 -8
- package/skills/developing-cpp/references/language.md +4 -3
- package/skills/developing-cpp/references/layout.md +21 -12
- package/skills/developing-cpp/references/naming.md +5 -4
- package/skills/developing-cpp/references/project-design.md +1 -1
- package/skills/developing-cpp/references/standard-library.md +2 -1
- package/skills/developing-cpp/references/types.md +1 -2
- package/skills/developing-cpp/scripts/checking-comments.mjs +371 -0
- package/skills/developing-cpp/steps/01-behavior.md +1 -1
- package/skills/developing-cpp/steps/02-boundary.md +4 -4
- package/skills/developing-cpp/steps/03-declaration.md +7 -7
- package/skills/developing-cpp/steps/04-implementation.md +6 -5
- package/skills/building-cpp/assets/cmake/applying-warnings.cmake +0 -10
- package/skills/building-cpp/assets/cmake/fetching-googletest.cmake +0 -15
- package/skills/building-cpp/assets/cmake/setting-rpath.cmake +0 -8
- package/skills/building-cpp/examples/cmake.md +0 -150
- /package/skills/building-cpp/assets/templates/projects/multi/src/{greeting.cc → greeting.cpp} +0 -0
- /package/skills/building-cpp/assets/templates/projects/multi/src/{main.cc → main.cpp} +0 -0
- /package/skills/building-cpp/assets/templates/projects/shared-library/greeting/src/{greeting.cc → greeting.cpp} +0 -0
- /package/skills/building-cpp/assets/templates/projects/shared-library/src/{main.cc → main.cpp} +0 -0
- /package/skills/building-cpp/assets/templates/projects/single/src/{main.cc → main.cpp} +0 -0
- /package/skills/building-cpp/assets/templates/projects/static-library/greeting/src/{greeting.cc → greeting.cpp} +0 -0
- /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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// 创建:
|
|
11
|
-
// 修改:
|
|
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
|
-
//
|
|
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
|
-
///
|
|
33
|
-
///
|
|
34
|
-
|
|
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
|
-
///
|
|
43
|
+
/// 井字棋棋盘。
|
|
43
44
|
///
|
|
44
|
-
///
|
|
45
|
-
///
|
|
46
|
-
///
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
69
|
+
/// 检查坐标是否在范围内。函数名用 UpperCamel 而不是 snake_case,与标准库区分。
|
|
70
|
+
[[nodiscard]] static bool IsInRange(CellPosition position) noexcept;
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
/// 检查坐标。
|
|
93
|
+
/// 返回格子类型;越界会抛异常。
|
|
94
|
+
CellType GetCellType(CellPosition position) const;
|
|
100
95
|
```
|
|
101
96
|
|
|
97
|
+
前两句都没有直接回答“合法时拿到什么、越界时发生什么”,读者要读完三句才拼出合同。
|
|
98
|
+
|
|
102
99
|
**好**:
|
|
103
100
|
|
|
104
101
|
```cpp
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
行尾写「存什么 + 初值含义 + 什么时候变」,同类成员对齐。
|
|
208
206
|
|
|
209
|
-
##
|
|
207
|
+
## 实现注释
|
|
210
208
|
|
|
211
209
|
**差**:
|
|
212
210
|
|
|
213
211
|
```cpp
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
std::size_t CountOverdue() const;
|
|
243
|
-
bool HasOverdue() const { return CountOverdue() > 0; }
|
|
244
|
-
};
|
|
245
|
+
// TODO: 参考 xxx
|
|
246
|
+
// FIXME: 这里可能有问题
|
|
245
247
|
```
|
|
246
248
|
|
|
247
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
283
|
+
// 2026-03-02 把 Board 从 Game 里拆出来,因为之前 Game 太长。
|
|
284
|
+
// 参见 https://example.com/2026/03/02/refactor
|
|
285
|
+
// 其余成员见下方注释,两处不重复。
|
|
286
|
+
// 本团队要求所有类注释写满五行。
|
|
287
|
+
```
|
|
256
288
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
**好**:
|
|
290
|
+
|
|
291
|
+
```cpp
|
|
292
|
+
//===----------------------------------------------------------------------===//
|
|
293
|
+
// board.h - 棋盘状态与规则判断
|
|
294
|
+
//
|
|
295
|
+
// 这一层只有格子状态和胜负判定;回合推进在 game.h,输入输出在 apps/。
|
|
296
|
+
//===----------------------------------------------------------------------===//
|
|
260
297
|
```
|
|
261
298
|
|
|
262
|
-
|
|
299
|
+
变更史进提交说明,外部资料进文档,团队流程进规范文件,跨文件的边界写文件头;类注释只写本类型自己的契约。
|
|
@@ -22,11 +22,11 @@ struct ParseResult {
|
|
|
22
22
|
|
|
23
23
|
[[nodiscard]] ParseResult TryParseTask(std::string_view line);
|
|
24
24
|
|
|
25
|
-
/// @throws
|
|
25
|
+
/// @throws std::out_of_range index 不在 [0, size()) 内。
|
|
26
26
|
[[nodiscard]] Task GetTask(int index) const;
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`TryParseTask` 把失败作为正常结果返回。`GetTask`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
+
**好**:两种模式都必须检查的 invariant 用 `Check`(项目保留时),失败时打印诊断并终止;只在开发期值得做、Release 可以省掉的自检用 `DebugCheck`。没有这两个宏时分别用 `assert` 与 `std::terminate`。
|
|
57
79
|
|
|
58
80
|
```cpp
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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` 可以留下;原棋盘不被写入。
|