-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
517 lines (271 loc) · 593 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>DarkSharpness's Dougen</title>
<subtitle>Home to Everyone</subtitle>
<link href="http://darksharpness.github.io/atom.xml" rel="self"/>
<link href="http://darksharpness.github.io/"/>
<updated>2025-01-04T06:47:30.324Z</updated>
<id>http://darksharpness.github.io/</id>
<author>
<name>DarkSharpness</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>dispatcher is all you need</title>
<link href="http://darksharpness.github.io/torch/"/>
<id>http://darksharpness.github.io/torch/</id>
<published>2024-11-26T18:08:43.000Z</published>
<updated>2025-01-04T06:47:30.324Z</updated>
<content type="html"><![CDATA[<p>咕咕咕, 等 OSDI 后再写. 如果你来 ping 我, 我会更快的更新.</p>]]></content>
<summary type="html">pytorch 写的是真的好. 本文会简单分析 torch 的 dispatcher, 以及实现在 C++ 侧拦截所有的 kernel.</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="Framework" scheme="http://darksharpness.github.io/categories/C/Framework/"/>
<category term="C++" scheme="http://darksharpness.github.io/tags/C/"/>
</entry>
<entry>
<title>杂七杂八的东西 - 大三上小结</title>
<link href="http://darksharpness.github.io/misc/"/>
<id>http://darksharpness.github.io/misc/</id>
<published>2024-11-04T09:10:49.000Z</published>
<updated>2025-01-08T08:17:12.598Z</updated>
<content type="html"><![CDATA[<blockquote><p>2024-12-25 更新, 打算直接把这学期的总结写这里算了, 懒得开新的帖子了, 之前是某个周末的日记</p></blockquote><p>转眼间学期都要结束了, 遂决定记录一下这学期的时间都浪费在了哪里. 没啥逻辑, 乱序记录 <del>大概率又是流水账</del>.</p><blockquote><p>剧透警告! 本文将涉及 TV 版 《寒蝉鸣泣之时》 和 《魔法少女小圆》 的部分剧情, 在观看前请谨慎!<br>为了读者的健康考虑, 本文不会放出任何血腥暴力图片, 但是可能含有微量恐怖元素.</p></blockquote><h2 id="前言-amp-真-·-总结"><a href="#前言-amp-真-·-总结" class="headerlink" title="前言 & 真 · 总结"></a>前言 & 真 · 总结</h2><p>出于某些原因, 笔者在大二一整年都没有更新自己的学期小结. 事实上, 笔者自认为大二的两学期几乎是虚度过去的, 在那时候, 笔者似乎一点都没有为未来做打算或考虑, 反而困于自己的精神内耗. 感谢 <a href="https://github.com/zsq259">hastin</a>, <a href="https://github.com/jpppppppppppppppppppppppp">jpp</a>, <a href="https://github.com/radioheading">Radioheading</a>, <a href="https://github.com/Wankupi">Wankupi</a> 等好友的陪伴(按字母序), 让我走过了那个迷茫的阶段, 他们都是超级 nice 的人. 那时候无名的痛苦已经不复存在, 笔者或许也应该去寻找自己未来的方向了. 不过在确定自己方向之前, 也该回顾以下自己做了些什么吧.</p><p>平心而论, 这学期我并没有使出 100% 的力气 (这里面有一些令人不悦的客观因素这里暂且不谈), 但是却感到了 200% 的疲惫. 笔者处于一种想要摆烂, 但是却摆不下来, 又卷不上去的状态. 首先是 10 月给开源项目 <a href="https://github.com/sgl-project/sglang">sglang</a> 和 <a href="https://github.com/mlc-ai/xgrammar">xgrammar</a> 打工. 感谢 <a href="https://github.com/hnyls2002">yls</a> 学长的介绍, 让我接触到了最顶尖的一批学者, 只是笔者一开始完全不懂得珍惜 (其实也是见识少, 没有意识到原来我自己也能 have an impact on the whole world), 写代码的时候并没有那么的全心全意的投入, 现在想来感到十分愧疚. 愧疚自然是没有用的, 还是需要靠自己的努力在未来去弥补的.</p><p>除了开源项目, 还有实验室和高年级学长的项目. 笔者在这里又坑了一波学长, 笔者在 9 月及以前对整个 storyline 的理解不够透彻, 导致走了很多的弯路, 一个 7 月开始的项目前三个月几乎就是废了, 这里面确实有我不小的责任. 幸运的是, 10 月份的时候笔者总算找对了方向, 项目代码也在一次次重构中可以跑起来了, 在 11 月末 12 月初经历了一系列紧张刺激的试验后 (e.g. 之前写炸写出了线程炸弹, 直接让集群宕机了一次 xs, 幸好当时没有别人在跑), 也总算是赶上了 OSDI 25 的 ddl. 只能说带我的老板真的吊, 一周时间直接把 paper 肝完了, 而且顾及到了很多笔者完全想不到的点, 笔者之后或许还需要多学学 writing (tell a story!). (小广告, 快来催更这个吧 <a href="/torch/"> dispatcher is all u need </a>)</p><p>在 12 月 OSDI 论文结束后, 笔者又忙着 rush 了 CV 大作业的一些任务以及其他不少之前落下的科目 (不过都不是很认真, 需要反思). 转眼间, 2025 年也到了, 笔者在新年的第一周开启了低功耗模式, 简单来说就彻底开摆, 甚至为此鼓起勇气和家长小吵了一场 (不过能获取家人的理解还是非常重要的). 难得的低压生活, 让笔者甚至有一点不太适应, 不过确实也是时候 take a good rest and prepare for the next leap 了.</p><p>笔者从两年前的寒假之后, 似乎就一直处于高压模式, 动机大概就是高考后遗症, 想要卷过身边的人, 但这是没有意义的. 还是应该寻找生活中 doki doki 的时刻的. 于是乎, 笔者便倾尽自己的全部, 写下了这些.</p><blockquote><p>写于 2025/01/08 16:00 UTC + 08, D19 107 寝室</p></blockquote><h2 id="Minecraft"><a href="#Minecraft" class="headerlink" title="Minecraft"></a>Minecraft</h2><p>在 9 月初暑假的两周, 在家没啥干劲, 每天唯一想做的事情就是和下一届的同学一起玩 mc. 虽然时间很短, 但是还是造了很多有趣的玩意的.</p><p>我主要管了以下这些玩意:</p><ul><li>恶魂塔 - 这玩意也造过 4,5 次了, 简单而有效, 从此火药不是梦想</li><li>岩浆怪/蛙鸣灯刷怪塔 - 这次我没偷懒, 把 5 层都造满了 <del>青蛙开心坏了</del></li><li>凋零玫瑰塔 - 虽然西瓜/南瓜的部分还没实装, 但是黑色染料是再也不缺了 (笑)</li><li>仙人掌农场 - 简单堆叠, 后面还可以再改进? 好看才是第一生产力.</li></ul><p>可以看得出, 笔者<del>就是</del>基本都是在摸鱼, 也没负责什么非常肝的机器. 不过笔者还是有幸见证了 “世界吞噬者” 的建造 & 运行过程, 的确是非常的震撼. 不过最开心的还是能够和同学们一起玩, 共同造有趣的东西. We create!</p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/281c2d1b197420c5.png" alt="猪人塔 by Hartmann_Psi"></p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/6c57664fd33fcf1d.png" alt="笔者的蛙鸣灯农村"></p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/e96e84e273dd8d1f.png" alt="世界吞噬者"></p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/f63106b6e7efffa1.png" alt="可怜的村民 (x)"></p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/9f7bafc7b89d7ccc.png" alt="蚌埠住了"></p><p>不过玩到最后还是遇到了每次玩纯生存遇到的巨大难题, 会陷入一种没的东西可造的境地. 虽然创造是无限的, 但是作为学生时间有限 <del>没有那么多肝</del>, 实在是没办法全心全意造非常 fancy 的机器, 而简单 plain 的机器也就那么一点点……</p><p>不过在 mc 中消磨时间虽然是无意义的, 但对我本人来说也是快乐的, 这份快乐贯穿了我的童年, 从小学到现在, 从来没有一个游戏能这样 catch me. 永远无法忘记, 小学每次往返老家的时候, 在车上玩手机玩 mc 的经历. 依稀记得某次从绍兴回来, 见到 mc 中漫天雪舞的场景, 那可能是童年最美的景色. <del>和我玩一辈子的 mc 吧😭</del></p><p>寒假或许还能玩玩服务器, 不过在有限的时间中, 如何创造, 如何给大家带来快乐, 如何避免玩到后来无事可做的困境, 这还是一个难题. 之前搞砸了太多的 mc 计划了, 感觉对自己已经不太有信心了, 不过我或许也没时间再 arrange 一次 mc 计划了, 大一的那些快乐时光看起来永远的离去了.</p><h2 id="寒蝉"><a href="#寒蝉" class="headerlink" title="寒蝉"></a>寒蝉</h2><blockquote><p>以下可能含有剧透内容, 请谨慎阅读.<br>以下内容潜在有血腥描述, 可能会导致阅读时感到不适, 请谨慎阅读!</p></blockquote><p>前情提要, 笔者是一个平时不怎么看番的人, 不过对一些知名的作品还是略有耳闻的. 早在笔者 18 年入坑东方的时候, 就有听过 “同人三大奇迹” 的寒蝉, 也听过 “蝉在叫, 人坏掉” 之类的描述, Foreground Eclipse 制作的二次创作同人曲再次增加了我对这个作品的印象, 不过由于高中忙着学习<del>制作音乐</del> 以及沉迷东方, 所以没有找到一个契机入坑.</p><p>说来也巧, 在 9 月开学前后, 由于 b 站刷到了一个相关的二次创作作品, 再加上开学那段时间实在是过于无聊, 便阴差阳错地开始看寒蝉鸣泣之时, <del>作为猎奇小鬼</del> 这种作品自然是不能错过的. 在观看前, 我简单了解了一下 TV 作品的架构, 分为出题篇和解题篇, 听起来挺像是侦探小说的样子, 然后就愉快的开看了.</p><h3 id="出题篇"><a href="#出题篇" class="headerlink" title="出题篇"></a>出题篇</h3><p><del>不亏是热血番</del> 第一集就给人当头一棒属实难绷, 然而后面的剧情可谓是更加的刀. 出题篇由鬼隐篇, 绵流篇, 祟杀篇, 暇溃篇组成, 不同的篇章有着类似的情节发展. 在看解答篇之前, 整部作品给我一种强烈的不和谐感: 先是<del>后宫番一样</del>惬意的日常校园生活, 每次却从一个人发疯开始, 然后是各种同伴之间的猜疑, 恐怖的人物崩坏, 渐渐开始有人失踪有人死亡, 直到最后整个村子的人全部死亡, 几乎无人幸免. 而且, 人们死的是如此的惨: 男孩拿棒球棒敲碎了同伴的头颅, 随后抓破了自己的喉咙而死; 可爱小女孩惨死神社门口, 甚至还被做了肠流 (慎查, 超级恐怖……).</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/6728ce6b6a064324.png" alt="蕾娜可爱"></p><p>在看完出题篇以后, 我的感觉是, 这可能是一个多时间线设定的作品, 而没有尽头的血腥和杀戮不断地紧绷着笔者的神经 (当时看完心里真的超级压抑沉重). 那些人为什么会突然发病? 究竟是谁, 是幕后黑手? 究竟是为何, 要让这些少年少女背上如此沉重的因果? 身边的同伴, 过于热心的警官, <del>少女心</del>医生和每年都会来的摄影师, 似乎每个人都有嫌疑. 带着问题 (是个好习惯 xs), 我继续了解题篇的观看.</p><h3 id="解题篇"><a href="#解题篇" class="headerlink" title="解题篇"></a>解题篇</h3><p>随着剧情发展,我才知道, 原来, 有一个叫做 “雏见泽症候群” 的东西, 似乎是一离开女王携带者的人就会发病, 这也解释了为什么会有人开始发病 (并不是由于什么诅咒). 而发病的背后, 其实是黑心医生在作祟. 所谓的医者仁心, 居然是为了让社会认可自己的研究, 不惜以整个村庄的人的生命作为代价, 制定了所谓的 “终末计划”, 既然自己得不到应有的认可, 那就要拉更多的人下水. 私欲完全盖过了人性, 做人的底线都丢失, 属实恶心. 而各位小伙伴病情恶化的主要因素, 也是互相之间的不信任: K1 怀疑蕾娜的饭里面有针, 魅音 (其实是诗音) 怀疑园崎本家是之前一系列诅咒作祟的元凶, 蕾娜怀疑朋友们不会帮自己保密…… 梨花, 作为时间的穿越者, 逐渐认识了事情的真相. 在大家克服了相互之间的猜疑之后, 终于齐心协力团结一心, 打破了命运的诅咒, 成功在某一条时间线中活过了那个无人生还的六月.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/d226ba2dd4f915bb.png" alt="-"></p><blockquote><p>感觉剧情概括似乎写太多了 x</p></blockquote><p>最后看完还是稍微有点小感动的 <del>(笔者看得少泪点低, 轻喷)</del>, 虽然最后基于奇迹才扳回一城的设定属实有点不太合理 (尤其是一开始我还差点把这个当本格推理番来看, 结果魔法都出来了…), 但是整个故事还是非常耐人寻味的. 解题篇基本很好的回复了前面出题篇的各个疑问, 梨花最后的点睛之笔 “这个世界不需要输家” 也是非常切合我当时的感想.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/5f78e9ce438666a6.png" alt="梨花"></p><h3 id="一些杂七杂八的感想"><a href="#一些杂七杂八的感想" class="headerlink" title="一些杂七杂八的感想"></a>一些杂七杂八的感想</h3><blockquote><p>笔者杂七杂八的想法, 没有任何深度</p></blockquote><p>整个故事其实就是经典的以小见大, 从一个小故事切入去分析背后隐藏的巨大的体制问题. 34 医生虽然是幕后黑手, 但她也是一个受害者, 一个从小苦命的孩子 (经典设定, 反派的苦衷, 笔者对此不做评价). 她努力学期, 渴望自己的研究被认可, 却发现人们认可的不过是自己的导师名气罢了. 人走茶凉的现实让她崩溃, 而一场黑暗的政治斗争又在她最绝望的时候给了她一个证明自己的计划, 只不过代价是一个村子的人的性命…… 从某种角度来看, 她自己也是一个悲惨的, 从来没有被真正认可, 甚至可以说的上是政治斗争的牺牲品的人. 没有人生来就是坏人, 只是被一时甚至一世的黑暗蒙蔽了双眼.</p><p>作品中梨花和其他小伙伴选择原谅了 34, 这大抵是寒蝉想表达的 “爱与信任” 的主题吧. 可能是笔者的觉悟不够, 但无论如何, 笔者始终无法认同、原谅其为此做出泯灭人性的行为.</p><p>除此之外, 作品中的流血表现, 属实是令人稍微有点生理不适. 老实说, 我一直有在质疑该类题材的作品的动机, 不过不得不说, 流血表现是黑暗人性的一种极端的体现, 那种刻在人基因中的恐惧给人带来的冲击力是无法取代的. 对于这个作品, 我认为这种极端的表现带来的效果是非常的好的, 足够发人深省. (虽然但是, 在后续作品 “业” 和 “卒” 中, 某些画面真的把我恶心坏了, 死的是难以形容的惨…… 不推荐看那两个新作)</p><blockquote><p>New Remark: 三个月前看的了, 剧情遗忘的有点厉害, 当初的感触也已经几乎消失殆尽了, 还是应该早点写的哎</p></blockquote><p>以及, 不得不说原作的结尾属实是有点生草, 几个小孩子打赢了”山狗”雇佣兵 (我打雇佣兵, 真的假的?), 这些属实是过于离谱了. 不过, 梨花的能力 “穿越时空” 本身其实就是一种奇迹, 通过作品推测, 所谓的结局不过是无数次轮回中唯一成功的那次, 所谓的 good ending 可能也只是奇迹的产物 (最后 34 近距离子弹射偏也证实了这一点). 虽然但是, 小伙伴齐心协力对抗命运, 成功打破命运的那一刻, 还是非常感人的, 奇迹与宿命.</p><p>根据某位同学的说法, 游戏的剧情描写似乎会更加全面一些, 不会像 TV 一样割掉了很多的内容. 不过笔者不太是一个能玩得来 galgame 类游戏的人, 或许以后有空了可能会去玩玩吧. 龙绘看起来也不错 (笑)</p><h2 id="小圆"><a href="#小圆" class="headerlink" title="小圆"></a>小圆</h2><blockquote><p>以下可能含有剧透内容, 请谨慎阅读.<br>以下内容潜在有血腥描述, 可能会导致阅读时感到不适, 请谨慎阅读!<br>在写这部分的时候, 刚写完前面寒蝉的总结, 笔者发现似乎不应该写那么多剧情相关的, 故后面的内容可能会掠过概括部分, 感兴趣的读者建议去看.</p></blockquote><p>前文也提到, 笔者是一个平时几乎不怎么看番的人, 所以小圆这种经典作品也是没有看过的. 也是不知道出于什么动机, 突然想去看小圆了, 于是乎笔者在两周的时间内看了 TV 和剧场版新/旧篇. 很多时候做一件事情似乎也不需要一个明确的动机. 笔者第一次听说小圆这个名词, 是在 <a href="/CP29/"> CP29 </a>. 依稀记得是在场馆靠近边缘的某家店, 老板娘提到她们是东方和小圆的粉丝, 从此 “小圆” 这个名词就深深地植入了我的脑子里. 大概是名字里面的 “魔法少女” 的缘故, 再加上年末一周实在没有干劲, 于是遂在 12/21 晚上开始看小圆 <del>圆神启动</del></p><p>内容就不过多概括了, 感兴趣的读者强烈建议去看原片 (b 站有, 不过不知道被和谐了多少). 只能说, 笔者确实算是被 “魔法少女” 四个字骗进来杀的臭现充.</p><p>一些小插曲: 在观看的过程中, <a href="https://github.com/Wankupi">Wankupi</a> 同学 突然发现小圆的爸爸和笔者的老板 <a href="https://www.cs.sjtu.edu.cn/~chen-quan/">quan chen</a> 稍微有点像, 仔细一看好像还真的有点, 可能是因为都是和蔼可亲的父亲的形象吧 (笑). 这里留给读者自行评判 x</p><div class="gallery-container" data-type="data" data-button=""> <div class="gallery-items">[{"url":"https://s3.bmp.ovh/imgs/2025/01/06/dab0d5daa4fb718a.png","alt":"小圆的爸爸"},{"url":"https://www.cs.sjtu.edu.cn/~chen-quan/IMG_0930-edited.jpg","alt":"笔者的老板"}]</div> </div><h3 id="TV-版本篇"><a href="#TV-版本篇" class="headerlink" title="TV 版本篇"></a>TV 版本篇</h3><blockquote><p>以下内容可能含有大量笔者地主观臆测, 请勿参考<br>写这一部分的时候, 反反复复的看了好几遍原片, 细节确实不少. 我也要成为圆学家了 (笑)</p></blockquote><p>老实说, 一开始看的时候, 我是以一个美少女<del>百合</del>番的心理预期去看的. 虽然第一集的 op 似乎暗示了战斗的大背景, 但是前几集的剧情走向似乎过于平和, 看不出很大的危险. 除了出现了一个叫做 “晓美焰” (又称 “小焰”) 的看起来和主角 “小圆” 敌对的人物, 看起来大家的关系都非常的和善. 资深巴麻美学姐带头消灭魔女 (与魔法少女敌对的物种), <del>老东西</del> QB <del>不是 quantum bit</del> 可以帮你实现成为魔法少女的愿望, 甚至还能额外满足你的一个愿望(原话: 和我签订契约, 成为魔法少女吧!), 魔法少女还有美丽的服装和超帅的变身动画 (笑). 直到这一幕……</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/7c22800c1e1ed7d0.png" alt="学姐掉头..."></p><p>说实话, 在音乐骤变的那一刻, 我大概就预料到了这一刻的到来, 但是还是没想到会这么有冲击力. 是啊, 光鲜亮丽的魔法少女背后, 是每天都要冒着生命危险和魔女战斗, 上一刻还在有说有笑的朋友, 下一刻直接就天人永隔. 学姐是一个孤独而脆弱的人, 她成为魔法少女是为了保住自己的性命 (她的家人在车祸中去世…). 在遇到小圆和沙耶香之前, 她是孤身一人在战斗, 可谓是每天走在生死线上. 她拼命锻炼来提升自己的能力, 努力让自己变得坚强. 小圆的到来让她以为自己有了同伴, “带着如此幸福的心情战斗还是第一次” <del>死亡 flag</del> 她丢失了自己的性命. 优雅的学姐就这样结束了自己悲惨的一生.</p><p>后面的故事是小圆、沙耶香、杏子、晓美焰几人支撑起来的. 在一次次与魔女的战斗中, 战斗残酷的一面也逐渐地暴露出来了. 不过最令人揪心的, 还是每个人面对的自己的困境. 沙耶香为了治好爱人的病症, 成为了魔法少女. 虽然嘴上说的是绝对不会为了自己而使用魔法, 但是所做的一切还是为了别人 <del>心上人上条恭介</del>. 她自己给自己设定了一个崇高的目标 (为了其他人而不求回报地奋斗, 代替之前学姐的位置), 内心却还是希望自己能和心上人在一起. 在心上人被自己曾经拯救过的挚友仁美夺舍之后, 她的内心濒临崩溃, 开始走向极端, 完全不顾身边好友的关心, 在战斗中自暴自弃.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/dc40523060784ac7.png" alt="口是心非的沙耶香"></p><p>笔者推测, 沙耶香实质上只是出于对于心上人的爱而许下了愿望, 却以不求回报的守护他人为借口默默地奋斗. 她其实非常希望自己能和恭介在一起, 但是好友的介入以及她自己的退避又让她十分痛苦. 她只能以责任感作为借口、战斗作为逃避, 却始终找不到自己战斗的意义. 在笔者的视角里, 她看起来是抛弃了一切的世俗的欲望, 但又放不下, 因为她根本没有这样的觉悟和动机啊 (她只是一个 14 岁的少女啊). 她的结局也是意料之外情理之中: 身上背负的诅咒太多, 灵魂宝石 soul gem 变得过于浑浊, 最终导致她变成了魔女 (魔法少女的末路是魔女). 为别人而祈愿, 希望能不求回报的奋斗, 但真实的心愿无法实现而感到痛苦. 在剧情中, 她无法忍受无法与爱人在一起、自己的身躯变成灵魂宝石 (设定: 魔法少女本体是 soul gem)、以及自认为守护的一切的肮脏 (最后黑化之前, 电车上听到的几个出生男人的对话), 她也意识到了自己行为与先前所说的 “正义的使者” 格格不入, 最终在价值破灭之后走向毁灭.</p><p>在看到这里的时候, 笔者其实对结局感到极其悲观. 这份绝望远胜于寒蝉, 至少寒蝉里面还看到有一些好转的迹象, 在出题篇结束的时候, 还有蕾娜靠意念正面硬刚 L5 的成功典范. 但在小圆里面, 学姐上来就死了, 沙耶香变成了魔女, <del>和沙耶香相爱相杀的</del>杏子试图唤醒魔女化的沙耶香无果, 最后也和沙耶香变成的人鱼魔女一起暴了. 主线只剩下小圆和晓美焰了. 扑面而来的宿命感, 为什么要让这些美少女背负如此沉重的因果, 我看不到明天的曙光.</p><p>在倒数第三集的时候, 也终于交代了全作中冷静的离谱的局外人 —— 晓美焰 的身世. 意料之中的, 她是一个时间穿越者 <del>怎么又是时间穿越</del>, 前面的剧情也有数不清的伏笔. 具体来说, 她有操控时间的能力, 可以暂停时间, 甚至把时间倒退 (虽然只看到她用来回到与小圆初次见面之前, 没看她用来短时回溯, 可能是防止能力太 bug 导致的). 在最初的时间线, 她只是一个娇弱的、害羞的麻花辫眼睛妹, 而在这个世界里面, 小圆则是一个非常热心开朗的女孩子, 早早的就和 QB 签订了契约成为了魔法少女, 也曾出手拯救了晓美焰.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/6c5c5d863e9b1609.png" alt="害羞 homura 酱, 和开朗的 madoka!"></p><p>在第一个时间线目睹了魔女之夜小圆的死之后, 她于悲痛中许下愿望, 希望能让与小圆的相遇重来, 能够保护小圆而不是让小圆保护她. 拯救小圆, 避免小圆的死, 拯救那个被 QB 欺骗成为魔法少女, 最后变成魔女的 “笨蛋” 小圆, 成为了晓美焰无数次读档重开的动力. “无数次轮回相同的时间, 寻找把你从绝望的命运中拯救出来的到道路”. 这份穿越时空的真挚的友情, 也是让笔者颇为触动.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/06/c09c21602ec6722a.png" alt="你能不能去救救那个还没被 qb 欺骗的那个笨蛋的我吗"></p><p>到头来, 一切都只是泡影. 在笔者看来, 变身魔法少女就是一个庞氏骗局. 签订契约成为魔法少女, 可以实现一个愿望, 但是带来的是与魔女无尽的战斗. 如果没能及时通过 grief seed (击败魔女的产物) 来恢复, 那么灵魂宝石 soul gem 就会变得污浊. 而最终的阶段就是成为魔女, 变成自己曾经最痛恨的敌人, 丧失作为人的一切理智. 事实上, 这个过程其实也就是魔法少女自己对内心真正愿望逐渐感到绝望的过程. 魔法少女因追求希望而生, 又在无法实现梦想的绝望中, 逐渐变得污浊, 最后沦为魔女. 整个设定其实是细思极恐的, 魔法少女必须击败魔女才能维持状态, 而魔女又是由魔法少女产生的…… QB 在宣传魔法少女的时候刻意隐瞒了这一点, 趁人之危让少女们许下心愿并为之战斗, 却从来没有透露一点关于成为魔法少女之后的改变的信息, 也难怪晓美焰每次轮回都忙着宰了这玩意.</p><p>在最后的这一次轮回中, 不出意外的, 晓美焰还是没能单刷魔女之夜. 历经了无数次轮回的历练, 她早已不再是当初那个羸弱的眼镜妹, 她摘下了眼睛, 收起了双马尾, 用冷酷的外表、干练的行动藏起了脆弱的内心. 但是, 一个人的力量终究是有限的. 更加绝望的是, QB 也指出, 小圆身上如此多的因果, 以至于成为最有潜力的魔法少女, 其实就是因为小焰一次次的轮回导致的. 这似乎在暗示着, 一切悲剧的源头还是晓美焰自身的愿望 (虽然笔者其实并没有看懂原作的这一段, 蹲一个解答). 再最后一次试图逆转时间的时候, 小焰犹豫了. 如果逆转时间, 小圆身上的因果又会增加, 这似乎只会给小圆带来更多的不幸.</p><p>剧情发展到这里, 我完全不知道这场悲剧将如何收尾. 如果小焰就此放弃了, 成为了魔女, 那自然是 bad ending, 唯一的逆转结局的棋子也丢失, 再也没有共命运抗争的武器了. 如果小圆成为魔法少女且最终没有变成魔女, 首先设定上是与之前无数次的结局相悖的, 其次这样会使得小焰无数次穿越时间的努力都将成为徒劳. 西西弗斯式的努力, 这一切似乎注定没有意义.</p><p>在最后的关头, 小圆出场了. 老实说, 虽然番名叫做 《魔法少女小圆》, 但实际上小圆的线非常的隐讳, 可以说整个故事的发展中, 小圆几乎就是一个旁观者. 她目睹了同伴的死亡、魔女化, 但晓美焰的阻拦、魔法少女的悲惨结局又使她没有迈出那一步. 事实上, 也得亏晓美焰一直在阻拦, 在笔者记忆中, 最开始的时间线中, 小圆似乎只是出于一个非常小的心愿就成为了魔法少女. 小圆本人其实并没有成为魔法少女的动机, 她身边有最好的同伴、幸福美满的家庭, 也没有一个突出的需要为之奋斗的目标, 可以想象她在前几个轮回中就是吃了 QB 的忽悠才成为了魔法少女. 直到最后一集之前, 圆的光辉都是被其他几个人盖住的, 剧情中塑造了性格鲜明的学姐、沙耶香、杏子、晓美焰, 让观众看到了她们的希望与绝望. 不过小圆也确实有在暗中发力, 她最早意识到小焰行为背后的苦衷, 也是在目睹了魔法少女的悲惨结局之后, 许下了愿望: “神什么的都好, 至今为止和魔女战斗的大家, 相信希望的魔法少女, 我不想再让她们哭泣, 希望她们都能笑着到最后. 任何妨碍这些的法则, 我会打破它, 重写它. 这就是我的祈愿, 我的愿望! 来! 实现它吧”. 于是小圆就成为了神一样的存在 <del>圆神启动</del>, 成功的改变了宇宙的法则, 重写了世界线, 成为了宇宙法则一样的概念 “圆环之理”. 在新的世界中, 所有的魔法少女不再会因为绝望而变成魔女, 而是在寻求希望的因果给世界带来诅咒之前, 被 “圆环之理” 带走. 而小圆的人生变得和宇宙一样永恒, 没有开始没有终结. 在新的世界里面, 没有人还记得小圆, 只有晓美焰还保有那份和鹿目圆在一起生活的记忆 <del>谁是真正的主角就不用我多说了</del>.</p><p><img src="https://s3.bmp.ovh/imgs/2025/01/08/ede976cd1466320f.png" alt="最后的祈愿"></p><p>笔者在看到这个结局的时候感到非常震撼, 小圆为了那些由希望而生的魔法少女, 做出了如此的牺牲. 正如晓美焰所说的, 这样比死掉还过分啊…… 笔者并不认为自己很好的理解了这个结局背后的意味. 或许在笔者有一些新的思考之后, 会来补全这一部分.</p><p>整体而言, 这是一部打着魔法少女噱头, 把笔者这种臭现充骗进来杀的黑暗番. TV 里面的主角都是青春期的少女, 她们大多有着非常悲惨的身世, 被 QB 诱骗成为了魔法少女. 而行为与最初愿望的开始偏离, 无法达到想要的结局, 便会催生绝望. 而小圆在最后下定了决心挺身而出, 在某种意义上拯救了所有魔法少女.</p><p>笔者看完该作品, 内心感到极其的沉重以及震撼, 遂写了一堆自己的感想. 感觉已经好久没有看到这样的作品了, 上次看到如此致郁系作品, 大概是 19 年的时候看到了恋恋的心跳大冒险, 也是给当时刚入东方的我迎头一击 (当然, 那个作品无论是细节, 还是思考的深度应该都远不及小圆, 毕竟只有一个人在创作), 也同样给读者带来了不少的思考. 我能在沙耶香身上见到自己曾经的影子: 有私欲, 但是没有勇气去实践, 遂转向一个宏大的志向 (不求回报的奋斗) 去逃避, 试图给找到自己存在的意义. 但是内心的情感是不会欺骗自己的, 我希望得到他人的认可 (或者某种形式的回报), 会因为无法得到而痛苦. 沙耶香是笔者很不想走向的结局, 至少现在, 笔者不应该草率做出一些影响终身的决定, 还是应当更加遵从内心. 晓美焰是笔者在剧中最喜欢的一个角色, 她有着明确的目标: 从无数次的轮回中, 找到救出小圆的办法. 她对小圆的那份炽烈的情感, 以及为之奋斗的执着, 真的令笔者不禁落泪 (当时看第十集的时候, 笔者真的被感动坏了).</p><p>无论如何, 魔法少女小圆, 在笔者的大学生活中算是留下了非常浓墨重彩的一笔. 这也是笔者第一次倾尽自己的情感, 记录自己的感受. 只能说这个作品完全对上了笔者的 XP.</p><h3 id="新约剧场版-叛逆的物语"><a href="#新约剧场版-叛逆的物语" class="headerlink" title="新约剧场版 - 叛逆的物语"></a>新约剧场版 - 叛逆的物语</h3><!--> 小圆 = madoka, 小焰 = homura.说实话, 在几乎一口气看完小圆 TV 版本的时候, 我就在感叹, homura 简直是我的神. 这份穿越了时空守护 madoka 的心意与勇气, 一度让我觉得她才应该是主角 ~~魔法少女小焰说~~. 个人感觉在人物的塑造上, homura 的那份炽烈的情感, 那份不变的意志, 贯穿了麻花辫娇弱少女和冷漠黑长直时期的小焰. 而直到最后才做出抉择, 守护世人的 madoka, 似乎总觉得表现力不 如 homura.--><p>To be continued… (主要是看的时候过于迷惑, 文中引用的 DaFort 的意象还没想的太清楚)</p><h3 id="一些其他的感想"><a href="#一些其他的感想" class="headerlink" title="一些其他的感想"></a>一些其他的感想</h3><p>To be continued… 大概会有: 1. 关于人物塑造(虽然笔者没啥鉴赏力). 2. 关于音乐(这个真的可以有!) 3. 作画 (精神污染, 不过有一些有意思的意象) 4. 一些有趣的细节</p><h2 id="live"><a href="#live" class="headerlink" title="live?"></a>live?</h2><p>12 月初狠狠地赶了一波 OSDI25 ddl, 之后又没停直接开冲网络考试. 意料之中的忙碌. 不过在最艰难的时候, 支撑着我的 <del>不仅有莉莉白</del> 一大主要动力大抵是 16 号的 live.</p><p>在 10 月份的时候, 就在 <a href="https://galneryus.jp/">Galneryus 乐队官网</a>上看到了 news 说要来中国公演, 然后几乎一点没犹豫就买了票. 考虑到上次乐队来中国似乎已经是 10 年前, 这次的机会显得额外的珍贵.</p><p>12 月 16 日上海场的 live 在五角场那边举行. 高德地图显示, 从交大出发过去大概需要 2h. live 是 20:00 开始, 所以我稍微提前了一点, 17:20 左右就出发了. 实际到那边的其实要快得多, 19:00 就到了.</p><p>作为一个耳朵尚未经过 livehouse 多次摧残的耳机党, 非常幸运的是我携带了降噪耳机, 这玩意拯救了我的耳膜. 在去之前两天看过有些人评论在广州/深圳场, 前排都要被震聋了, 于是决定携带耳机. 这玩意确实能极大的降低外界的音量 (至少降低到一个比我平时听歌响一点的量级), 坏处是对高频的损失有点严重, 感觉 4000Hz 附近的声音衰减的厉害, 不过所有丢失的部分都能脑补就是了, 专辑还是听过很多遍的 (笑).</p><p>live 上的 band 的表现还是非常不错的. syu 确实和 <del>传说</del> 传言中一样, 现场弹的比录音室还要稳, solo 一点都不拖泥带水的, 弹得非常干净 (要是能弹得像他 20 年前那样稳感觉我也无敌了). syu 人也巨幽默, 表情过于丰富, 经常会做出令人稍微有点难绷的表情, 不过其本人的陶醉其中也是真实的. 也终于知道 yuhki 小可爱这个称号怎么来的了, 一直慈祥(?)的笑着看观众, 中间 Emotions 那首和 syu 的对飙 solo 也是酷爆了. 小野老师终究也是成了老野老师, 声音没有 10 年前刚进 Galneryus 时候那么的有力了, 但是感觉状态似乎比最近在日的几场 live 要好 (之前看到前两个月他们在日的 live, 老野的高音感觉完全崩了……), 一场下来感觉除了上五组没以前那么轻松, 基本没什么问题, 火力全开! 值得一提是, 在某一首曲子开始前, 老野看到某位前排观众在录像, 直接把他的手机拿上台, 对着台下拍了一圈, 感觉那位老哥估计当时激动疯了 (笑). bass 手 TAKA 似乎容易被人忽略, 但是琴是真的帅, 不知道是不是耳机削高频的缘故, 感觉现场 bass 超级很猛的, 中间的一些高速段落也能分辨出其律动. 鼓手小哥 LEA <del>看起来非常年轻小鲜肉</del> 全程感觉底鼓没停过 (轰轰轰), drum solo 打的是酣畅淋漓, 中间和小野老师的 “好吃” 二人转也是非常好笑的, 可惜笔者最后没捡到扔出去的鼓棒 (x).</p><p>encore 的曲目是 Heartless, Raise My Sword, Ultimate Sacrifice. 乐队 encore 的状态依旧不减, 迫真火力全开. 虽然最后还是没等到 Angel of Salvation 或 Destiny 这两首神曲, 但是还是非常满意的, 毕竟见到了我学琴的偶像 (是的, 学琴的一大 motivation 就是 syu, 高速而流畅的段落 <del>眼泪倒流</del>).</p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/fd0761073534a3f1.jpg" alt="从水源源友那里偷来的图. 笔者在图中前排偏左方向"></p><p>现场听曲子, 也和耳机听有不一样的感受. 开场 The Reason We Fight 直接炸飞, 全场一起和声直接力量感拉满. 中间的 Emotions 更是绚爆了, 喜闻乐见的键盘和吉他对飙 solo, syu 现场即兴的小三度推弦也别有一番韵味, 紧绷的情绪在尖叫一般的推弦中完全释放了出来, 爽! In Water’s Gaze, 结尾同样味道的推弦, 三拍子的绝美忧伤旋律, 就是那味! 令我印象最深刻的曲子是 Voice In Sadness, 虽然这种听起来比较简单重复的曲子, 感觉和近几年 G 团越来越复杂的节奏型稍微不符, 在耳机听的时候我还觉得稍微有点呆 (日本小灵云), 结果现场才发现是真的又欢乐又炸 <del>果然还是最基本的节奏型好甩起来</del>, 算是意外的收获啦! I Believe, 主打曲, 每张专辑的保留节目, 也没让我失望. 近几年这几张的主题似乎都挺忧伤的, 15 年的 The Force of Courage 给人的是十足力量感 <del>团结就是力量</del>, 即使梦想破灭也要坚持, 17 年的 Ultimate Sacrifice 则是忧伤为主, 拼尽全力追求无法实现的梦想的绝望与坚持, 而今年 (2024) 这张感觉像是两张 mix 在一起, 无尽的宿命感…… <del>力量金属是这样的, 主题高度相似, 不过不得不说笔者就是很吃这一套</del></p><p>笔者最早听 Galneryus 也是从最热门的几首, Angel of Salvation, Raise My Sword, Destiny 听起的. 笔者感觉他们的曲子最有意思 (或者说最吸引笔者) 的一点是曲子中的那份凄凉和宿命感, 以及同命运抗争的力量, 最有意境的当属 17 年的 Ultimate Sacrifice. 这份凄凉不止体现在词, 还有小野老师的那份高昂的唱腔一份功劳, 以及 syu 的凄美 guitar solo, yuhki 的键盘营造的飘渺的、天国般的氛围感, 以及由 bass 和 drum 构建的严实、密不透风的低频音墙. 正如这张专辑封面所描绘的, 骑士们在悬崖末路, 向着宿命举起了他的利剑. 那份明知不可为而为之的勇气, 理想无法达成的绝望, 是笔者认为他们作品最能牵动笔者心弦的一点. (稍微有点中二了 hh)</p><p><img src="https://www.metal-archives.com/images/6/6/6/8/666820.jpg?4326" alt="Ultimate Sacrifice, 图片来自 metal archive"></p><h2 id="老的前言"><a href="#老的前言" class="headerlink" title="老的前言"></a>老的前言</h2><blockquote><p>注: 本部分的时间背景是在 2024/11/04</p></blockquote><p>好久没更新了, 放一点最近的一些游玩经历.</p><p>笔者本人属于半个宅, 平时出去频率不是特别高, 再加上学校的事情也不少, 基本没怎么出去玩, 不过生活中还是有不少 doki doki 的时刻的.</p><p>图片晚点再补. 写的比较乱. 写不动长文了.</p><h2 id="换弦"><a href="#换弦" class="headerlink" title="换弦"></a>换弦</h2><blockquote><p>注: 本部分的时间背景是在 2024/11/04</p></blockquote><p>前情提要: 笔者由于事情比较多, 弹琴频率其实不是非常的高. 不过, 每次回家还是会摸起来练一练基本功啥的.</p><p>之前本来想着买一套防锈的弦, 这样一来可以减少换弦的频率, 虽然最后还是选择 ernie ball 家的弦. 他们家的弦似乎不是那么的防锈 (也可能是我的无情硫酸手导致的 x), 大概在用了一个月后 2 弦就有稍有发黑. 音色方面, 笔者虽然不是 5 年琴龄的老 guitarist, 但是依然能一耳朵听出来与之前廉价的 Alice 琴弦的区别, 个人感觉低音弦低把位 (7 品之前) 会稍微有一点钢筋味, 然后高把位的高音会更加有特色 (加上失真后, 是一种呜呜呜的音色, 没有之前那么刺耳), 总之很戳我.</p><p>虽然但是, 当时由于年轻, 暂时还不清楚如何上弦, 发生了不少意外. 首先, 由于我当时年幼无知, 不知道要预留多少弦, 导致有些弦的余量甚至不足以绕弦栓一圈. 幸运的是, 我的弦有自锁旋钮, 于是我就把自锁拧的特别紧, 来避免弦松动 (常规操作时多绕弦栓几圈).</p><p>然而, 这个操作埋下了一个伏笔. 在拉紧一弦的时候, 它断了, 就断在自锁弦钮的位置, 疑似是自锁弦钮压得太紧, 弦的张力又过大导致的. 虽然没有对我造成人身伤害, 但是给我幼小的心灵带来了巨大的心理阴影 (x). 最后还是拿官方送的备用弦 (不得不说, 很贴心), 小心翼翼地重上了一遍一弦. 这次我讨巧了一点, 所有弦先 drop 了半音 (E A D G B E -> D# G# C# F# A# D#), 即先调整到 Standard D#, 然后再集体往上调了一个半音, 调回正常的 Standard E.</p><p>这大概就是笔者上一次换弦的经历, 至今大概过了半年多了.</p><p>在国庆回家的时候, 笔者不幸的发现, 二三弦已经锈的不成人样了, 因此笔者打算趁双 11 的机会买一套新的弦.</p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/6b5b6aef57ba1ff8.jpg" alt="锈烂咯"></p><p>这次稍微思考了一下, 选择了达达里奥的不锈钢镀镍的琴弦, 主要是期望它能用的更久. 旧换新, 总是一件令人开心的事情.</p><p>吸取了上次上弦的惨痛教训, 这次笔者给每条弦预留了大概一个品左右的余量 (目测 3cm 多一点?). 参考了若干 bilibili 上的视频, 笔者在每条弦绷直的过程中, 在琴头用力下压琴弦, 使得弦上保证有一定的张力, 从而避免了琴弦绕弦柱绕的不够紧的问题 (虽然但是, best practice 应该是用卷弦器).</p><p>有了上次断弦的经历, 这次笔者一开始就没有把弦拉的特别紧, 只要一条弦能绷直就直接上下一条. 最后调完的时候, 大致调弦大致是 Standard C# 左右, 主要还是上一次断弦的经历过于吓人了, 心理阴影太大了…… 在休息片刻后, 笔者再逐个地把每条弦调整到了正常的 Standard E 调弦的音高. 虽然看很多指导视频有说, 要对称的上弦, 而且要边上边调制之前已经上好的弦, 不然张力不平衡容易断, 但似乎笔者并没有遇到类似的问题 (笔者的思考大致是这样的: 张力越大, 琴体越弯, 会使有效弦长变短, 进而降低张力. 这是一个负反馈过程, 每上完一条弦, 只会让之前的弦张力变小, 所以不按顺序/不随时调整应该不会带来灾难性后果).</p><p>上完了后小玩了一会儿, 不得不说达达里奥的这套新弦的音色确实戳我. 低音弦钢筋味 (嘎吱嘎吱, 当然更有可能是打品的声音) 非常足, 由于时间匆忙没上失真测试, 所以高音弦尚不知道情况. 不过感觉高音弦没有上次的 EB 的新弦那么滑, 上次上完 EB 的新弦后, 感觉自己是滑弦大师 (bushi). 在手感上, 新弦也会更舒服一点, 记得当时 EB 的新弦弹久了以后, 会感觉手上有点扎 (?), 但这一套弦完全没有这样的感觉 (这一点暂时存疑, 可能是当时技术太差了不适应琴弦导致的).</p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/ed5f8d7b87bd924d.jpg" alt="剪之前"></p><p>最终把多余的琴弦剪掉后, 琴头看起来还是很整齐的, 也是非常的赏心悦目. 最有成就感的一集 <del>(我写代码怎么就没这么有成就感呢)</del></p><p><img src="https://s3.bmp.ovh/imgs/2024/12/25/0325e58b579f07c5.jpg" alt="剪之后"></p><blockquote><p>注: 笔者不是打广告 (也没这实力), 以上纯属个人经历, 仅供参考</p></blockquote><h2 id="团日"><a href="#团日" class="headerlink" title="团日"></a>团日</h2><blockquote><p>注: 本部分的时间背景是在 2024/11/04</p></blockquote><p>在上完弦以后的一天, 就感冒了. 大概率是淋雨. 一天基本没做什么事情, 大概看了一眼 DiT (Diffusion Transformer) 和稍微写了点代码, 睡得很早, 因为第二天要团日, 参观天文馆.</p><p>笔者先前还是非常期待团日的呢, 毕竟这可能是最后一次和同学们一起出游的机会了, 在这之后大家还能再一起玩几次呢…… 离别的时间感觉已经近在咫尺.</p><p>团日是在周日, 早上 7 点半左右起来, 睡前看了眼地图, 说到滴水湖需要大约 2 个小时, 遂在 8 点出发. 天气和预报的一样, 寒冷刺骨 (也可能是穿两件太少). 周末, 地铁上人确实少. 乘坐 7 号线到龙阳路, 然后换乘 16 号线. 虽然笔者是上海人, 早就听闻 16 号线, 但是一直没有动机去坐一次, 这次总算是找到一个理由. 16 号线和普通的地铁线路还稍有不同, 座位的排布更像是火车, 基本上是横着的 (即乘客面向的方向与列车前进的方向相同或相反, 而不是垂直的), 在靠近车厢连接处的座位倒是和普通地铁一样的排布 (参考: 地铁 6 号线).</p><p>16 号线基本上都是在地上的轻轨, 偶尔有两三站是在地下, 所以大部分时间我都在盯着窗外看风景. 城市景致笔者自然是没什么兴趣的, 个人觉得在一片平坦的区域里, 耸立的大楼会显得很突兀, 很丑. 城市也就集群可能看起来有一种现代美, 而在偏郊区甚至农村的地方, 高楼就像是一把利刃无情地刺穿了大地的皮肤. 上海还是太拥挤了, 房价也太高了…… 一路上, 见到不少看起来像是高中甚至初中生的小朋友, 年纪轻轻就要拖着巨大行李箱, 笔者猜测可能是周一到周五(六)在市区上学, 然后周天回家休息一天……</p><p>实际上, 地铁的速度还是非常快的, 实际上大概只花了 1.5 个小时就到了滴水湖(全程有 1 小时左右在 16 号线上). 不得不吐槽一句, 滴水湖的地铁站是真的破…… 4 号出口给人一种废弃的荒地的感觉. 在我的记忆里, 2 号线靠近浦东机场的几站 (比如凌空路) 同样如此, 有时候城市呆久了, 很难想象在不远处就是如此不现代, 但又是如此的现实……</p><p>由于早到了半小时, 所以现在周围简单逛了一圈. 空气非常的清新, 虽然大概率是心理作用, 出去走一走总是好的. 附近基本和我印象中的小城镇差不多, 基本上都是一些底层平房, 没有特别多的高楼, 同样麻雀虽小五脏俱全, 该有的设施应有尽有.</p><p>简单参观了一圈外围后, 同学们也到了, 排队进馆内参观. 一进入天文馆, 过去作为自然科学人的 DNA 立刻被激活了. 有一瞬间确实稍有触动, 大学选择了一个与自然科学无关的学科, 但是心中对于它的热爱从未消失 (<del>和我学一辈子物理吧 😭</del>). 上次大量的接触天文知识可能也是很小的时候, 那时候每天最喜欢的做事情就是看各种科普杂志 (<del>可能也有一堆伪科学</del>), 了解身边世界和宇宙的奥秘. 可惜这份热爱最终还是没能支撑的最后, 甚是遗憾.</p><blockquote><p>突然联想到之前在准备学子讲坛的时候, 看到的一些关于 ZUN (东方 Project 的作者) 关于科学世纪的看法. 或许过于关注科学, 甚至把科学作为生活的唯一准则, 会抑制我们的幻想. 曾经作为一个极端的理科人, 确实很难体会到 ZUN 的秘封作里面想要传达的意思. 可能还是在稍微冷静 (?) 一点之后, 才能意识到其中的深意吧 (<del>ZUN: 我就喝多了乱写的</del> <del>ZUN 就一个酒鬼, 他懂啥东方</del>).</p></blockquote><p>总的来说, 天文馆如同我老板评价的一样, 稍微有一点降智, 不过其中关于天体的小知识还是有很意思的, 无论是 10 岁的 DarkSharpness, 还是 20 岁的, 一样都会沉醉其中. 接下来的环节就是和同学到处逛, 到处骑车 (爽), 然后看风景. 虽然一开始定下的绕湖骑行一圈的目标未能达成, 但是还是如愿摸到了滴水湖的水, 水温刚刚好. 非常美的景色, 可惜不太会拍照, 不过最美的景色已经刻在了回忆里面了.</p><p>虽然但是, 感觉很多时候和同学在一起不会有特别强的, 归属感或者连接感, 哪怕是最亲近的几位, 虽然同样是在一起玩, 但是却没有了往日的那种欢乐. 在线下, 在我冷静的时候, 可能还是需要和其他人保持一个中等距离 (虽然大部分时候我都挺癫的), 也不知道什么时候开始我也变得有点自闭了 (x).</p><p>返程路上, 无聊把最近听一张专辑再从头刷了一遍. 由于学 (mo) 业 (yu) 繁 (tai) 忙 (duo), 已经很久没有放空大脑, 专注地去听一张专辑了, 重听也确实找到了不少有意思的点. 不得不说 G 团 (Galneryus) 风格已经基本上定型, 新专一听就是那味道, syu 的花样感觉玩的也差不多了 (当然, 也可能是最近一年听的太多了). 小野老师终究还是变成了老野 (毕竟一个 57 岁的人了). 键盘 solo 也没啥亮眼的表现了, 只能说没有全程熄火就已经很满意了.</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><del>笔者度过了幸福的一生</del></p>]]></content>
<summary type="html">好久没更新了</summary>
<category term="随笔" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
<category term="总结" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/%E6%80%BB%E7%BB%93/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>我讨厌配环境</title>
<link href="http://darksharpness.github.io/env/"/>
<id>http://darksharpness.github.io/env/</id>
<published>2024-07-25T12:51:42.000Z</published>
<updated>2024-07-27T13:50:59.000Z</updated>
<content type="html"><![CDATA[<p>你说得对, 但是又到了经典的配环境时刻. 每次配环境都是一个令人头疼的过程, 在没有配好环境写不了代码的时候, 内心总是感到非常焦急, 只能干楞着却做不了事情. 参考了 Conless 的配环境 <a href="https://conless.dev/blog/2024/server-cheatsheet/">cheat sheet</a>, 笔者决定也记录一下万恶的配环境之路.</p><p>以下的所有环境配置默认是在 Linux 进行.</p><h2 id="shell"><a href="#shell" class="headerlink" title="shell"></a>shell</h2><p>在第一次连上远程服务器的时候, 需要配置 shell, 否则连 vscode 都直接通过 ssh 连上去.</p><p>我拿到的服务器集群默认 shell 是 zsh, 笔者由于之前没有使用过 zsh, 因此完全按照的是 <a href="https://www.wankupi.top">Wankupi</a> 同学的建议配置的, 简单来说就是 zsh + oh-my-zsh. 过程几乎完全参考 <a href="https://ohmyz.sh/">https://ohmyz.sh/</a>, 只需要一条指令即可:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</span><br></pre></td></tr></table></figure><p>笔者不太熟悉 zsh, 如果想要为 zsh 添加更多更强的插件, 请参考前面 Conless 的 <a href="https://conless.dev/blog/2024/server-cheatsheet/">blog</a></p><p>在配置好最简单的 shell 环境以后, 就可以通过 VSCode remote-SSH 连接到主机, 快乐的 coding & exploring 了!</p><h3 id="一些笔者新学会的小知识"><a href="#一些笔者新学会的小知识" class="headerlink" title="一些笔者新学会的小知识"></a>一些笔者新学会的小知识</h3><blockquote><p>小贴士: 可以试着 <code>chmod 700 .</code>, 保护你的文件, 阻止别人看到你的文件!</p></blockquote><p>我们的集群中, 新开的用户的目录默认权限是 755. 这是很危险的, 因为 5 = 4 | 1, 表示其他人可以读/执行你的文件. chmod 的每一位可以是 0 ~ 7 中的任何一个数, 其中 bit 0 (1) 表示执行权限, bit 1 (2) 表示写权限, bit 2 (4) 表示读权限. 你可以通过 <code>stat .</code> 查看当前目录的权限.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">第一行含有 Access: (0755...</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">其中 0755 是当前的权限</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">当然, Linux only</span></span><br><span class="line">stat . | grep Access</span><br></pre></td></tr></table></figure><p>一般有四个数字 (从高到低的第一个貌似经常省略 (?)), 有如下含义:</p><ol><li>第一个数字: 笔者不知道, 貌似不常用.</li><li>第二个数字: 表示文件所有者的权限.</li><li>第三个数字: 表示文件所属组的权限.</li><li>第四个数字: 表示其他用户的权限.</li></ol><p><del>不过貌似集群会定期重置为 0755, 貌似没啥用</del></p><h2 id="miniconda"><a href="#miniconda" class="headerlink" title="miniconda"></a>miniconda</h2><p>笔者需要配 python 环境, 自然是逃不了配 conda. 笔者选择的是 miniconda. 这个东西的安装也是非常的简单, 没有什么技术难度, 直接按照 <a href="https://docs.anaconda.com/miniconda/">miniconda 官网</a>的教程来就行了.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mkdir -p ~/miniconda3</span><br><span class="line">wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh</span><br><span class="line">bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3</span><br><span class="line">rm -rf ~/miniconda3/miniconda.sh</span><br></pre></td></tr></table></figure><p>最后, 别忘记了给自己的 shell 注入一下 conda 的初始化 setup :).</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">bash version</span></span><br><span class="line">~/miniconda3/bin/conda init bash</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">zsh version</span></span><br><span class="line">~/miniconda3/bin/conda init zsh</span><br></pre></td></tr></table></figure><p>在这以后, 就可以正常的 conda 初始化了~</p><h2 id="cuda"><a href="#cuda" class="headerlink" title="cuda"></a>cuda</h2><p>幸运的是, 集群是有必要的 cuda 环境的, 这免去了我二次坐牢的体验. 为什么是二次呢, 因为在这之前几天, 我刚刚在本机上配了 cuda 的环境, 简直是一个坐牢. 这里简单讲一讲我在本机 Windows + WSL2 环境下是如何配置 cuda 环境的.</p><p><del>首先, 你必须要有nvidia 显卡和驱动</del> 一般来讲, 一台有 nvidia 的显卡, 安装了驱动后, 可以在命令行输入 nvidia-smi 查看驱动信息. 笔者笔记本上的 cuda 驱动信息大致如下所示:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">+---------------------------------------------------------------------------------------+</span><br><span class="line">| NVIDIA-SMI 546.80 Driver Version: 546.80 CUDA Version: 12.3 |</span><br><span class="line">|-----------------------------------------+----------------------+----------------------+</span><br><span class="line">| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |</span><br><span class="line">| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |</span><br><span class="line">| | | MIG M. |</span><br><span class="line">|=========================================+======================+======================|</span><br><span class="line">| 0 NVIDIA GeForce RTX 3050 ... WDDM | 00000000:01:00.0 On | N/A |</span><br><span class="line">| N/A 47C P5 8W / 60W | 1796MiB / 4096MiB | 15% Default |</span><br><span class="line">| | | N/A |</span><br><span class="line">+-----------------------------------------+----------------------+----------------------+</span><br><span class="line"></span><br><span class="line">+---------------------------------------------------------------------------------------+</span><br><span class="line">| Processes: |</span><br><span class="line">| GPU GI CI PID Type Process name GPU Memory |</span><br><span class="line">| ID ID Usage |</span><br><span class="line">|=======================================================================================|</span><br><span class="line">| 0 N/A N/A 2148 C+G ...e\VSCode\Microsoft VS Code\Code.exe N/A |</span><br><span class="line">+---------------------------------------------------------------------------------------+</span><br></pre></td></tr></table></figure><p>非常的低配, 但是勉强够用了(真生产力还是得靠集群).</p><p>在此之后, 需要安装对应版本的 cuda toolkit. 比如, 笔者在 WSL2 安装的就是 cuda 12.3 toolkit. 安装方法也是非常的简单, 直接在浏览器搜索: cuda toolkit 12.3, 然后跟着教程来. 例如 <a href="https://developer.nvidia.com/cuda-12-3-0-download-archive?target_os=Linux">cuda toolkit 12.3</a></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">WSL-Ubuntu 的安装方法, 具体请参考本机配置及官网</span></span><br><span class="line">wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb</span><br><span class="line">sudo dpkg -i cuda-keyring_1.1-1_all.deb</span><br><span class="line">sudo apt-get update</span><br><span class="line">sudo apt-get -y install cuda-toolkit-12-3</span><br></pre></td></tr></table></figure><p>在此之前, 笔者曾试图安装和驱动 cuda version 不一致的 cuda toolkit 12.5, 结果在编译的时候喜提链接错误以及头文件缺失, 所以保险起见还是暂时先安装同版本的 toolkit.</p><p>特别注意的是, 对于部分较老的 cuda toolkit, 其不一定支持新版本的 gcc, 这意味着你可能需要把你的 gcc 降级. 笔者用 gcc-13 + nvcc 12.3 编译会提示不支持, 如果强行忽略会出现链接错误, 降低版本到 gcc-12 后解决.</p><p>笔者本地管理版本用的是 update-alternatives, 可以方便的在不同版本的 gcc 中切换 (即, 使得默认的 gcc —version 得到不同的结果,).</p><p>在此, 笔者特别感谢 <a href="https://conless.dev/">Conless Pan</a> 同学在这过程中给予的帮助!</p><h2 id="git-ssh-gpg"><a href="#git-ssh-gpg" class="headerlink" title="git + ssh + gpg"></a>git + ssh + gpg</h2><p>配完 conda, 笔者第一件做的事情就是 <code>git clone</code> 仓库, 然后发现本地并没有 ssh-key :(.</p><p>配置 ssh-key 也不难, 只需要按照 github 的官方指令来就行了, <a href="https://docs.github.com/zh/authentication/connecting-to-github-with-ssh">链接在此</a>, 重点是生成新的 ssh-key 以及添加到 github.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">请把下面字符串中的邮箱替换成你自己的 :(</span></span><br><span class="line">ssh-keygen -t rsa -b 4096 -C "[email protected]"</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">把打出来的东西直接加到 github 的 ssh-key 里面即可</span></span><br><span class="line">cat ~/.ssh/id_rsa.pub</span><br></pre></td></tr></table></figure><p>当系统提示 “Enter a file in which to save the key” 时,按 Enter 键接受默认文件位置, 简而言之就是一路默认.</p><p>当然, 之前在本机 WSL 环境下配置了 gpg, <del>github 签名 verified 是真的好看</del>, 所以我们也需要在新的设备上配置 gpg. 这一步也不难, 同样是按照 github 的官方教程来, <a href="https://docs.github.com/zh/authentication/managing-commit-signature-verification/generating-a-new-gpg-key">链接在此</a>.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">gpg --full-generate-key</span><br><span class="line">gpg --list-secret-keys --keyid-format=long</span><br></pre></td></tr></table></figure><p>执行指令的时候会有一些操作, 反正一切跟随默认原则即可. 在执行完以下两条指令后, 应该会看到类似的输出:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">------------------------------------</span><br><span class="line">sec 4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10]</span><br><span class="line">uid Hubot <[email protected]></span><br><span class="line">ssb 4096R/4BB6D45482678BE3 2016-03-10</span><br></pre></td></tr></table></figure><p>这时, 根据想要的 key 生成密钥, 例如上述例子 (来自前面那个官方例子), 如果选择第一个, 那就是:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gpg --armor --export 3AA5C34371567BD2</span><br></pre></td></tr></table></figure><p>此时, 复制以 ——-BEGIN PGP PUBLIC KEY BLOCK——- 开头并以 ——-END PGP PUBLIC KEY BLOCK——- 结尾的 GPG 密钥. 将 GPG 密钥新增到 GitHub 帐户, 即可.</p><p>当然, github 的教程是到此为止了, 但是本地的配置其实还不够. 为了让我们在 git commit 时自动跳出窗口, 并且强制输入密码采用 gpg 签名, 我们还需要一些额外的设置.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">强制每次 commit 都要 gpg 签名</span></span><br><span class="line">git config --global commit.gpgsign true</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">修改 ~/.zshrc 或者 ~/.bashrc</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">这是为了让 gpg 在 git commit 的时候弹出窗口</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">否则会报错, 只能通过命令行签名.</span></span><br><span class="line">export GPG_TTY=$(tty)</span><br></pre></td></tr></table></figure><h2 id="Small-summary"><a href="#Small-summary" class="headerlink" title="Small summary"></a>Small summary</h2><p>一切按照官方教程, 基本不会出错. 遇事不决请选择 default. 前人的试错经验是非常珍贵的, 再次感谢 Conless, Wankupi 等同学给予的莫大的帮助!</p>]]></content>
<summary type="html">配环境, 然后破防.</summary>
<category term="计算机" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
<category term="工具" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E5%B7%A5%E5%85%B7/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>(Modern) C++ 小技巧汇总</title>
<link href="http://darksharpness.github.io/cpp/"/>
<id>http://darksharpness.github.io/cpp/</id>
<published>2024-06-09T15:01:00.000Z</published>
<updated>2024-07-25T17:38:02.000Z</updated>
<content type="html"><![CDATA[<p>众所周知, 笔者 (DarkSharpness) 是一个 Modern C++ 的狂热爱好者. 笔者自高中信息竞赛以来, 主力编程语言一直都是 C++, 在大学的学习过程中, 积累了不少的实践经验, 故开一个帖子计划长期维护. 每次更新会在头部显示.</p><h2 id="bit-field"><a href="#bit-field" class="headerlink" title="bit-field"></a>bit-field</h2><p>这个其实算不上 modern C++ 的部分, 这是 C 继承下来的一个重要 feature.</p><p>在一些偏底层且空间/性能敏感的领域, 我们可能需要把多个数据压缩存储到一起. 举个例子, int4 量化的时候, 我们可能需要把 8 个 4 bit 的数(表示范围是 -8 ~ 7)压缩到一个 int 中 (4 * 8 = 32). 再比如说, 在嵌入式开发中, 某些硬件寄存器每个 bit 可能对应不同的 flag, 我们在读出这个寄存器的值的时候, 可能需要把这些 flag 读出来.</p><p>以上这些需求, 最容易想到的做法是使用位运算, 取出一个数字的特定几位. 然而, 这样的代码难以维护, 各种左右移, 以及掩码操作, 稍微复杂一点代码就会变得难以阅读, 即使设计了对应接口, 其直观性还是一般, 如下所示.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">int4_8</span> {</span><br><span class="line"> <span class="type">int</span> data;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取第 i 个 4 bit 数</span></span><br><span class="line"><span class="keyword">template</span> <<span class="type">int</span> which></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">get</span><span class="params">(<span class="type">const</span> int4_8 &x)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> (x.data >> (which * <span class="number">4</span>)) & <span class="number">0xf</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把第 i 个 4 bit 数设置为 value</span></span><br><span class="line"><span class="keyword">template</span> <<span class="type">int</span> which></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">set</span><span class="params">(int4_8 &x, <span class="type">int</span> value)</span> </span>{</span><br><span class="line"> x.data &= ~(<span class="number">0xf</span> << (which * <span class="number">4</span>));</span><br><span class="line"> x.data |= (value & <span class="number">0xf</span>) << (which * <span class="number">4</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们希望我们能想操纵一个普通的变量那样, 操控一些 bit. 遗憾的是, 计算机中的最小寻址单元是 byte, 我们并不存在 bit 的引用. 但是, C++ 提供了一个很好的解决方案: bit-field. 我们可以使用 bit-field 来定义一个结构体, 其中的成员变量可以指定其占用的 bit 数, 如下所示.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">int4_8</span> {</span><br><span class="line"> <span class="type">int</span> x0 : <span class="number">4</span>; <span class="comment">// 0 ~ 3 bit</span></span><br><span class="line"> <span class="type">int</span> x1 : <span class="number">4</span>; <span class="comment">// 4 ~ 7 bit</span></span><br><span class="line"> <span class="type">int</span> x2 : <span class="number">4</span>; <span class="comment">// 8 ~ 11 bit</span></span><br><span class="line"> <span class="type">int</span> x3 : <span class="number">4</span>; <span class="comment">// 12 ~ 15 bit</span></span><br><span class="line"> <span class="type">int</span> x4 : <span class="number">4</span>; <span class="comment">// 16 ~ 19 bit</span></span><br><span class="line"> <span class="type">int</span> x5 : <span class="number">4</span>; <span class="comment">// 20 ~ 23 bit</span></span><br><span class="line"> <span class="type">int</span> x6 : <span class="number">4</span>; <span class="comment">// 24 ~ 27 bit</span></span><br><span class="line"> <span class="type">int</span> x7 : <span class="number">4</span>; <span class="comment">// 28 ~ 31 bit</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> int4_8 x;</span><br><span class="line"> x.x0 = <span class="number">1</span>;</span><br><span class="line"> x.x1 = <span class="number">-3</span>;</span><br><span class="line"> std::cout << x.x0 << <span class="string">" "</span> << x.x1 << std::endl;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过这样的方式, 我们可以直接访问到一个 int 中的特定几位, 而不需要手动进行位运算. 我们可以在 <a href="https://en.cppreference.com/w/cpp/language/bit_field#:~:text=The%20type%20of%20a%EE%80%80%20bit-field%EE%80%81">cppreference</a> 上查看到更多关于 bit-field 的细节.</p><p>在笔者的实践中, 一般不会太在意 cppreference 上说到的所有细节, 但是笔者认为以下这些还是比较重要的:</p><p>首先, bit-field 的类型必须是整数类型. 这还是比较好理解的, 因为其本质就是对于整数位运算的某种语法糖.</p><p>其次, 如果希望达到节约空间的目的, 被压缩在同一个 int 中的 bit-field 之和显然不能超过 int 的 bit 数量, 超过的 bit-field 部分一般来说会被放到下一个 int 中. 自然, 这中间可能存在一些 padding, 以保证对齐.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">bit_pack</span> {</span><br><span class="line"> <span class="type">int</span> x : <span class="number">16</span>;</span><br><span class="line"> <span class="type">int</span> y : <span class="number">14</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 这里有 2 bit 的 padding,</span></span><br><span class="line"> <span class="comment">// 因为下一个 z : 16 放不下了, 第一个 int 只剩下2 bit</span></span><br><span class="line"> <span class="comment">// 请注意, 这是一个实现定义行为, 不同的编译器可能会有不同的行为.</span></span><br><span class="line"> <span class="comment">// 一般的编译器还是会选择不要让 z 跨越两个 int</span></span><br><span class="line"> <span class="comment">// 因为如果跨越 int 存储, 会导致访问效率降低, 性能下降</span></span><br><span class="line"></span><br><span class="line"> <span class="type">int</span> z : <span class="number">16</span>;</span><br><span class="line"> <span class="type">int</span> : <span class="number">15</span>; <span class="comment">// 手动添加 padding, 不需要名字</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 这里有 1 bit 的 padding, 因为无论如何都要对齐到 int</span></span><br><span class="line"> <span class="type">int</span> w;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>当然, bit-field 也支持类型混用, 即不一定要是同一种整数类型, 但是要求整数的位宽相同, 否则会先把前面的类型 padding 到整数位宽, 然后再放入后面的类型.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">bit_pack_2</span> {</span><br><span class="line"> <span class="type">int</span> x : <span class="number">16</span>; <span class="comment">// 16 bit</span></span><br><span class="line"> <span class="type">unsigned</span> y : <span class="number">16</span>; <span class="comment">// OK, 和 x 在同一个 int 中</span></span><br><span class="line"> <span class="type">int</span> z : <span class="number">8</span>;</span><br><span class="line"> <span class="comment">// 这里有 24 bit 的 padding,</span></span><br><span class="line"> <span class="comment">// 因为 uint8_t 只有 8 bit, 和 int 不一样</span></span><br><span class="line"> <span class="comment">// 因此, 前一个 int 会先被 padding 到 32 bit</span></span><br><span class="line"> <span class="comment">// 再放入 uint8_t</span></span><br><span class="line"> <span class="type">uint8_t</span> w : <span class="number">3</span>;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>说到这里, 就不得不提 C++ 中的 <code><bit></code> 这个头文件了. 这个头文件是 C++ 20 新增的, 其提供了一些 bit 操作的函数, 如 <code>std::bit_cast</code>, <code>std::rotl</code>, <code>std::rotr</code>, <code>std::countr_zero</code>, <code>std::countr_one</code> 等等. 这些函数可以帮助我们更加方便地进行 bit 操作. 基本上, 你能想到的 bit 操作, 这个头文件都有.</p><h2 id="string-switch"><a href="#string-switch" class="headerlink" title="string switch"></a>string switch</h2><p>在 C/C++ 中, 你应该用过 <code>switch</code> 语句, 其可以高效而直观地表示多分支的逻辑. 但是, <code>switch</code> 语句只能接受整数类型的参数, 不能接受字符串类型的参数.</p><p>我们自然是无法从语言层面上改变什么, 但是我们可以基于已有的技术实现一个类似的 <code>string_switch</code>. 注意到 <code>switch</code> 里面只能接受整数或者枚举类型, 我们的思路就是把字符串转换为整数或者枚举类型. 一个非常 naive 的思路是用 <code>std::unordered_map</code> (或者 <code>std::map</code>) 来实现. 但是, 这样可能存在一些问题: 首先 <code>std::unordered_map</code> 并不支持 <code>constexpr</code> 的静态对象, 因为其涉及了动态内存分配. 而且, <code>case</code> 里面的整数也要求是 <code>constexpr</code> 的, 如何在编译器就能得到具体的哈希值, 如何解决哈希冲突, 都是需要考虑的问题.</p><p>虽然 <code>constexpr std::unordered_map</code> 看起来是不行了, 但是这个思路是没问题的. 我们最核心的思路就是把字符串转化为可以枚举的整数类. 因此, 我们可以自己手写一个 <code>hash</code> 函数, 或者调用 <code>std::hash</code> 函数, 来得到一个 <code>constexpr</code> 的整数值, 然后我们只需要存储这些整数值就行了. 如下所示:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <std::<span class="type">size_t</span> _Base = <span class="number">131</span>></span><br><span class="line"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="built_in">my_hash</span>(std::string_view view) -> std::<span class="type">size_t</span> {</span><br><span class="line"> <span class="keyword">auto</span> hash = std::<span class="type">size_t</span>{<span class="number">0</span>};</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> c : view) hash = hash * _Base + c;</span><br><span class="line"> <span class="keyword">return</span> hash;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">example</span><span class="params">(std::string_view input)</span> </span>{</span><br><span class="line"> <span class="keyword">switch</span> (<span class="built_in">my_hash</span>(input)) {</span><br><span class="line"> <span class="function"><span class="keyword">case</span> <span class="title">my_hash</span><span class="params">(<span class="string">"hello"</span>)</span>:</span></span><br><span class="line"><span class="function"> std::cout << <span class="string">"hello"</span> << std::endl;</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="function"><span class="keyword">case</span> <span class="title">my_hash</span><span class="params">(<span class="string">"world"</span>)</span>:</span></span><br><span class="line"><span class="function"> std::cout << <span class="string">"world"</span> << std::endl;</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> std::cout << <span class="string">"default"</span> << std::endl;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这就是我们的实现的原型了, 事情似乎有点太简单了. 现实中, 可能并没有这么简单. 对于任意输入的字符串, 我们可能需要考虑哈希冲突的问题. 对于要 match 的那些字符串, 如果出现了冲突, 在编译期间就会直接出错, 而我们只需要简单的把模板中的 <code>_Base</code> 替换一下就行了. 比较麻烦的是, 即使我们进入了某个 <code>case</code>, 我们也不能保证输入的字符串和要匹配的一样. 我们需要额外的判等.</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">example_1</span><span class="params">(std::string_view input)</span> </span>{</span><br><span class="line"> <span class="keyword">switch</span> (<span class="built_in">my_hash</span>(input)) {</span><br><span class="line"> <span class="function"><span class="keyword">case</span> <span class="title">my_hash</span><span class="params">(<span class="string">"hello"</span>)</span>:</span></span><br><span class="line"><span class="function"> if (input =</span>= <span class="string">"hello"</span>) {</span><br><span class="line"> std::cout << <span class="string">"hello"</span> << std::endl;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="function"><span class="keyword">case</span> <span class="title">my_hash</span><span class="params">(<span class="string">"world"</span>)</span>:</span></span><br><span class="line"><span class="function"> if (input =</span>= <span class="string">"world"</span>) {</span><br><span class="line"> std::cout << <span class="string">"world"</span> << std::endl; </span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> std::cout << <span class="string">"default"</span> << std::endl;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样以后, 其基本就是一个完美的 <code>string switch</code> 了, 有需要的话可以自行修改 <code>my_hash</code> 函数. 但是, 我们还是要手写一遍判等, 这样非常麻烦, 而且容易出错. 这时候, 我们可以请出 C 语言的最终杀器: 宏. 以下是作者自己的实现:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">example2</span><span class="params">(std::string_view input)</span> </span>{</span><br><span class="line"> <span class="meta">#<span class="keyword">define</span> match(str) \</span></span><br><span class="line"><span class="meta"> case my_hash(str): <span class="keyword">if</span> (input != str) break; <span class="keyword">else</span></span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">switch</span> (<span class="built_in">my_hash</span>(input)) {</span><br><span class="line"> <span class="built_in">match</span>(<span class="string">"hello"</span>) {</span><br><span class="line"> std::cout << <span class="string">"hello"</span> << std::endl;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="built_in">match</span>(<span class="string">"world"</span>) {</span><br><span class="line"> std::cout << <span class="string">"world"</span> << std::endl;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="built_in">match</span>(<span class="string">"return"</span>) <span class="keyword">return</span>; <span class="comment">// Allow one-liner</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> std::cout << <span class="string">"default"</span> << std::endl;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">#<span class="keyword">undef</span> match</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然, 既然都用到宏了, 自然可以再结合 VA_ARGS 来实现更加通用的 <code>string switch</code>, 如果有需求可以自己定制.</p><p>简而言之, 借助 <code>constexpr hash</code> 函数, 以及宏, 我们可以实现一个类似于 <code>string switch</code> 的功能. 如果有需求, 也可以自行修改.</p><h2 id="assert-in-C"><a href="#assert-in-C" class="headerlink" title="assert in C++"></a>assert in C++</h2><p>如果你写过 C, 那你可能用过 <code>assert</code> 这个宏, 用于在运行时检查某个条件是否满足, 如果不满足, 则会终止程序, 并且详细地输出错误信息. 但是, 既然我们都用了 C++ 了, 为什么不用 C++ 的方式来实现呢?</p><p>首先, 我们先看一下 C 的 <code>assert</code> 都输出了些什么. 文件, 行号, 函数名…… 这些在 C++ 里面怎么获取呢? 如果用 <code>__LINE__</code> 这类 C 里面的宏, 那又违背了我们的初衷. 幸运的是, C++ 20 提供了 <code>std::source_location</code> 类, 可以获取到文件名, 行号, 函数名等信息. 以下是一个简单的实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">assert</span><span class="params">(_Tp &&condition,</span></span></span><br><span class="line"><span class="params"><span class="function"> std::source_location location = std::source_location::current())</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (condition) <span class="keyword">return</span>;</span><br><span class="line"> std::cerr << <span class="string">"Assertion failed: "</span> << location.<span class="built_in">file_name</span>() << <span class="string">":"</span></span><br><span class="line"> << location.<span class="built_in">line</span>() << <span class="string">" "</span> << location.<span class="built_in">function_name</span>() << std::endl;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然, 这样的实现可能还有一些不够完美的地方. 用户不能自定义输出信息, 光秃秃的报错信息可能不够友好. 而如果要在运行时生成输出信息字符串, 可能又会映入不小的性能开销. 因此, 我们应该支持 assert 传入多个参数来自定义输出信息. 以下是一个更加完善的实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp, <span class="keyword">typename</span>... _Args></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">assert</span><span class="params">(_Tp &&condition, _Args &&...args,</span></span></span><br><span class="line"><span class="params"><span class="function"> std::source_location location = std::source_location::current())</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (condition) <span class="keyword">return</span>;</span><br><span class="line"> std::cerr << <span class="string">"Assertion failed: "</span> << location.<span class="built_in">file_name</span>() << <span class="string">":"</span></span><br><span class="line"> << location.<span class="built_in">line</span>() << <span class="string">" "</span> << location.<span class="built_in">function_name</span>() << std::endl;</span><br><span class="line"> <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(<span class="keyword">sizeof</span>...(args) > <span class="number">0</span>)</span> </span>{</span><br><span class="line"> (std::cerr << ... << args) << std::endl;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然而, 如果你真的这么写了, 你会发现这种代码无法通过编译. 这是因为在调用 <code>assert</code> 的时候, 类型替换会失败. 你传入的最后一个参数会被尝试与 <code>std::source_location</code> 匹配, 但是显然是不行的. 这听起来非常令人沮丧, 难道我们在每个调用处都必须要手写一个 <code>std::source_location::current()</code> 吗? 当然不是! 除了函数模板, 我们还有类模板. 配合类模板的推导模板, 我们可以实现这个功能. 以下是一个完整的实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp, <span class="keyword">typename</span>... _Args></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">assert</span> {</span><br><span class="line"> <span class="built_in">assert</span>(_Tp &&condition, _Args &&...args, std::source_location location = std::source_location::<span class="built_in">current</span>()) {</span><br><span class="line"> <span class="keyword">if</span> (condition) <span class="keyword">return</span>;</span><br><span class="line"> std::cerr << <span class="string">"assert failed: "</span></span><br><span class="line"> << location.<span class="built_in">file_name</span>() << <span class="string">":"</span> << location.<span class="built_in">line</span>() << <span class="string">": "</span> << location.<span class="built_in">function_name</span>() << <span class="string">": "</span>;</span><br><span class="line"> <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(<span class="keyword">sizeof</span>...(args) != <span class="number">0</span>)</span></span></span><br><span class="line"><span class="function"> <span class="params">((std::cerr << args), ...)</span> << std::endl</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp, <span class="keyword">typename</span>... _Args></span><br><span class="line"><span class="built_in">assert</span>(_Tp &&, _Args &&...) -> assert<_Tp, _Args...>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">assert</span>(<span class="number">0.1</span> + <span class="number">0.2</span> == <span class="number">0.3</span>, <span class="string">"Hello, World!"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了避免使用危险的宏, 我们最后只能选择了这种扭曲的方式实现了我们的 <code>assert</code>. 但是, 这种方式也有一些优点. 首先, 我们可以自定义输出信息, 而且只有在错误时才会生成输出字符串, 而不会有性能开销. 如果你觉得太丑了, 你甚至可以借助 <code>format</code> 来实现更加优雅的格式化输出. 其自由度还是非常高的, 比起 C 原生的 <code>assert</code>. 最后, 附上一个使用了 <code>format</code> 的实现:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp, <span class="keyword">typename</span>... _Args></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">assert</span> {</span><br><span class="line"> <span class="built_in">assert</span>(_Tp &&condition, std::format_string <_Args...> fmt = <span class="string">""</span>, _Args &&...args,</span><br><span class="line"> std::source_location location = std::source_location::<span class="built_in">current</span>()) {</span><br><span class="line"> <span class="keyword">if</span> (condition) <span class="keyword">return</span>;</span><br><span class="line"> std::cerr << <span class="string">"assert failed: "</span></span><br><span class="line"> << location.<span class="built_in">file_name</span>() << <span class="string">":"</span> << location.<span class="built_in">line</span>() << <span class="string">": "</span> << location.<span class="built_in">function_name</span>() << <span class="string">": "</span>;</span><br><span class="line"> std::cerr << std::format(fmt, std::forward<_Args>(args)...) << std::endl;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp, <span class="keyword">typename</span> _Fmt, <span class="keyword">typename</span>... _Args></span><br><span class="line"><span class="built_in">assert</span>(_Tp &&, _Fmt &&, _Args &&...) -> assert<_Tp, _Args...>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">assert</span>(<span class="literal">false</span>);</span><br><span class="line"> <span class="built_in">assert</span>(<span class="number">1</span> + <span class="number">1</span> == <span class="number">3</span>, <span class="string">"wtf {} {}"</span>, <span class="number">1</span> + <span class="number">1</span>, <span class="number">3</span>);</span><br><span class="line"> <span class="built_in">assert</span>(<span class="number">0.1</span> + <span class="number">0.2</span> == <span class="number">0.3</span>, <span class="string">"Hello, World!"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">本文是笔者写 C++ 代码得出的一些实践经验,会长期更新</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/categories/C/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
<category term="C++" scheme="http://darksharpness.github.io/tags/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>GDB 使用笔记</title>
<link href="http://darksharpness.github.io/gdb/"/>
<id>http://darksharpness.github.io/gdb/</id>
<published>2024-03-02T04:05:14.000Z</published>
<updated>2024-05-24T04:05:14.000Z</updated>
<content type="html"><![CDATA[<p>写 Kernel 的时候,需要用到 GDB + QEMU 调试,这里记录一些 GDB 的常用指令,会动态更新。</p><h2 id="简单安装"><a href="#简单安装" class="headerlink" title="简单安装"></a>简单安装</h2><p>首先,需要安装 <code>riscv64-unknown-elf-gdb</code>。作为一个懒狗,笔者参考了 rcore tutorial 的<a href="https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter0/5setup-devel-env.html#gdb">安装教程</a>。需要注意的是,按照该教程下载完 <code>.tar.gz</code> 文件后,需要解压,然后把解压后 /bin 里面的 <code>riscv64-unknown-elf-gdb</code> 移动到 <code>/usr/local/bin</code> 下即可。</p><h2 id="启动指令"><a href="#启动指令" class="headerlink" title="启动指令"></a>启动指令</h2><p>默认是 riscv64-unknown-elf-gdb。QEMU 采用默认端口 1234。其中 xxx 是可执行文件路径。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">riscv64-unknown-elf-gdb \</span><br><span class="line"> -ex <span class="string">'file xxx'</span> \</span><br><span class="line"> -ex <span class="string">'set arch riscv:rv64'</span> \</span><br><span class="line"> -ex <span class="string">'target remote localhost:1234'</span></span><br></pre></td></tr></table></figure><h2 id="调试指令"><a href="#调试指令" class="headerlink" title="调试指令"></a>调试指令</h2><div class="table-container"><table><thead><tr><th>指令</th><th>参数</th><th>作用</th><th>缩写</th></tr></thead><tbody><tr><td>backtrace</td><td>-</td><td>查看函数调用栈</td><td>bt</td></tr><tr><td>breakpoint</td><td>(*addr)</td><td>在地址 addr /当前位置设置断点</td><td>b</td></tr><tr><td>continue</td><td>-</td><td>继续执行程序,直到断点</td><td>c</td></tr><tr><td>delete</td><td>(num)</td><td>删除第 num 个/所有断点</td><td>d</td></tr><tr><td>disable</td><td>(num)</td><td>禁用第 num 个/所有断点</td><td>dis</td></tr><tr><td>enable</td><td>(num)</td><td>启用第 num 个/所有断点</td><td>e</td></tr><tr><td>info</td><td>…</td><td>显示具体信息</td><td>i</td></tr><tr><td>print</td><td>expr</td><td>显示表达式的值</td><td>p</td></tr><tr><td>x</td><td>addr</td><td>显示内存地址 addr 的内容</td><td>x</td></tr><tr><td>list</td><td>-</td><td>显示当前执行的代码是哪个文件</td><td>l</td></tr><tr><td>step</td><td>(num)</td><td>执行 num/单行代码, 会进入函数</td><td>s</td></tr><tr><td>next</td><td>(num)</td><td>执行 num/单行代码, 会跳过函数</td><td>n</td></tr></tbody></table></div><p>需要注意的是, 如果 <code>next</code> 和 <code>step</code> 后添加后缀 <code>i</code>, 即 <code>nexti</code> 和 <code>stepi</code>, 则对应的是汇编指令级别的执行 (即一条汇编指令), 而不是 C 代码级别的执行 (即一行 C 代码), 遇到函数的处理和 <code>next</code> 和 <code>step</code> 是类似的, 缩写对应的是 <code>ni</code> 和 <code>si</code>.</p><p>以下是一些常用指令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">x/4 0x11451400 <span class="comment"># 显示内存 0x11451400 地址开始的 4 个 word</span></span><br><span class="line">x/5i <span class="variable">$pc</span> <span class="comment"># 显示当前 pc 往后 5 条指令。i 表示显示汇编指令</span></span><br><span class="line"> <span class="comment"># $pc 这种形式可用于显示寄存器的值</span></span><br><span class="line">x/3i <span class="variable">$pc</span> + 4096 <span class="comment"># 显示 pc + 4096 地址开始的 3 条指令</span></span><br><span class="line"> <span class="comment"># 事实上,参数貌似可以是任意表达式</span></span><br><span class="line">p/d <span class="variable">$t0</span> <span class="comment"># 显示寄存器 t0 的值, /d 表示以 10 进制显示</span></span><br><span class="line">p/x 114514 <span class="comment"># 显示 114514 的 16 进制表示,不过这么做挺无聊的...</span></span><br><span class="line">p/t 114514 <span class="comment"># 显示 114514 的 2 进制表示</span></span><br><span class="line">p/c 48 <span class="comment"># 显示 48 的字符表示 (这里是'0')</span></span><br></pre></td></tr></table></figure><p>这里简单总结一下显示数值类型的参数:</p><div class="table-container"><table><thead><tr><th>参数</th><th>显示格式</th><th>寻址单位 (byte)</th></tr></thead><tbody><tr><td>/a</td><td>十六进制</td><td>8</td></tr><tr><td>/b</td><td>不变</td><td>1</td></tr><tr><td>/c</td><td>字符</td><td>1</td></tr><tr><td>/d</td><td>十进制</td><td>不变</td></tr><tr><td>/f</td><td>浮点数</td><td>4 或 8</td></tr><tr><td>/h</td><td>不变</td><td>2</td></tr><tr><td>/i</td><td>指令</td><td>4</td></tr><tr><td>/o</td><td>八进制</td><td>不变</td></tr><tr><td>/s</td><td>字符串</td><td>字符串</td></tr><tr><td>/t</td><td>二进制</td><td>不变</td></tr><tr><td>/u</td><td>无符号</td><td>不变</td></tr><tr><td>/w</td><td>不变</td><td>4</td></tr><tr><td>/x</td><td>十六进制</td><td>不变</td></tr></tbody></table></div>]]></content>
<summary type="html"><p>写 Kernel 的时候,需要用到 GDB + QEMU 调试,这里记录一些 GDB 的常用指令,会动态更新。</p>
<h2 id="简单安装"><a href="#简单安装" class="headerlink" title="简单安装"></a>简单安装</h2><p</summary>
<category term="计算机" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
<category term="工具" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E5%B7%A5%E5%85%B7/"/>
<category term="调试" scheme="http://darksharpness.github.io/tags/%E8%B0%83%E8%AF%95/"/>
</entry>
<entry>
<title>玄学优化和语言知识 2.0</title>
<link href="http://darksharpness.github.io/optimize2/"/>
<id>http://darksharpness.github.io/optimize2/</id>
<published>2023-11-11T03:11:11.000Z</published>
<updated>2023-12-22T04:35:26.000Z</updated>
<content type="html"><![CDATA[<p>写了编译器以后,对于 C++ 的理解又进步了不少,看 Compilor Explorer 的汇编也轻松了许多,也对于优化有来些更加深刻的认识。</p><p>下面将会讨论一些有趣的问题,会比较零散。</p><h1 id="移动语义与右值"><a href="#移动语义与右值" class="headerlink" title="移动语义与右值"></a>移动语义与右值</h1><h2 id="为什么要有这玩意"><a href="#为什么要有这玩意" class="headerlink" title="为什么要有这玩意"></a>为什么要有这玩意</h2><p>原因大概是这样的: 对于复杂的对象,其可能本身不大( <code>sizeof</code> 的大小),但是其本身管理了一些指针,那些指针指向了大量的数据,体积可能是数百倍之于本身。最常见的就是 STL 中的各种容器,比如 <code>std::set</code>,<code>std::vector</code>,<code>std::list</code> 甚至是 <code>std::string</code> 等等。</p><p>以 <code>std::vector</code> 为例,其本质上是保存了指向数据区的三个指针来进行维护。在我们拷贝一份 <code>std::vector</code> 的时候,我们可能会提出这样的问题: 如何高效地拷贝 <code>std::vector</code> 中的数据。一般情况下,我们肯定想的是直接拷贝,用 <code>std::copy</code> 把数据复制一份。</p><p>复制的开销当然是巨大的,这时候,聪明的你可能会发现,我们其实不一定要拷贝数据,我们可以只保存指向那些数据的指针即可。是的,这是一种合理的解决方案,在原来的 <code>std::vector</code> 失效之前,我们的确可以用指针来访问原来的数据。当然,C++ STL 提供了一个不错的包装: 迭代器 (其实大多就是指针的包装…)。不同容器的迭代器提供了一种通用的容器视图,用 <code>begin()</code> 和 <code>end()</code> 两个迭代器表示一个区间。</p><p>当然,迭代器的方案并不是严格意义上的拷贝。那些迭代器只是对于实际容器区间的一个视图,其与实际的数据的生命周期无关。换句话说,原有数据的“掌控权”还是在老的 <code>std::vector</code> 里面,当这个 <code>std::vector</code> 发生改变(比如扩容,析构)时,这个迭代器视图可能会失效。因此,真正的拷贝肯定不能这么写。</p><p>但是,对于那些生命周期步入尾声的容器 <code>std::vector</code>,其该操作之后数据即将被析构,不会再被用到。在这样的情况下,我们可以考虑“接管”这个 <code>std::vector</code>,即让其在析构的时候不去释放数据,而是把数据的掌控权交给新的 <code>std::vector</code>。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">std::vector <<span class="type">int</span>> <span class="built_in">func</span>();</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">work</span><span class="params">(std::vector <<span class="type">int</span>> &)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 这里的 func() 返回的是一个临时对象</span></span><br><span class="line"> <span class="comment">// 在这个赋值表达式结束后,这个临时对象在析构前不会再被用到</span></span><br><span class="line"> <span class="comment">// 因此,我们可以考虑接管这个临时对象的数据 </span></span><br><span class="line"> std::vector <<span class="type">int</span>> tmp = <span class="built_in">func</span>();</span><br><span class="line"> <span class="built_in">work</span>(tmp); </span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而这,便是移动操作。而那些可以被移动的对象,便是右值,其可以被右值引用绑定。而 C++ 则进一步扩大的程序员自由发挥的空间。除了那些显然生命周期即将结束的临时变量可以被右值引用绑定,我们也可以通过 <code>static_cast</code> 把一个左值引用转化为右值引用,通过类型的不同来调用不同的函数。这样,我们可以统一对于那些在这个函数之后管理的数据不会再被用到的类型进行针对性的优化。</p><p>例如本例 <code>std::vector</code> ,可以直接把新的 <code>std::vector</code> 的指针指向原来的 <code>std::vector</code> 的数据,再把原来的 <code>std::vector</code> 的指针设置为空,使得这个数据完全被新的 <code>std::vector</code> 接管。</p><h2 id="一些性质"><a href="#一些性质" class="headerlink" title="一些性质"></a>一些性质</h2><p>移动操作其实随处可见。例如,正常的一个函数调用会返回一个临时变量,而它显然在这个操作之后,就不会再被用到,因此,其显然是一个右值,可以被右值引用绑定。又比如说,类型转化后(无论是隐式类型转化还是显式的),其返回的也是一个临时变量,也是一个右值。</p><p>这些天然右值显然可以绑定到右值引用。当然,前面也讲到,我们可以通过 <code>static_cast</code> 把一个左值引用转化为右值引用。当然,<code>static_cast</code> 还是太长了点,所以标准库提供了 <code>std::move</code> 作为一个语法糖,其实现几乎就是 <code>static_cast</code>。</p><p>那右值的意义何在?为什么要强转为右值引用?因为其决定了重载协议!我们可以根据是左值还是右值来决定是否取走的引用对象的数据,从而实现移动语义。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">std::string str = <span class="string">"abc"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果 str 确认不会再被用到了,那么我们可以把 str 的数据移动到 a 中</span></span><br><span class="line"><span class="comment">// 在 move 以后,str 就不再拥有数据了</span></span><br><span class="line"><span class="comment">// 按照 cpprefence 的说法,数据处于一种合法但是未定义的状态,至少可以保证析构不会出错</span></span><br><span class="line">std::string a = std::<span class="built_in">move</span>(str);</span><br></pre></td></tr></table></figure><p>对于那些天然的右值,比如函数返回值(包括类型转化函数),我们自然是不需要 <code>std::move</code> 来要求移动语义。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">work</span><span class="params">(std::string)</span></span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">test</span><span class="params">()</span> </span>{</span><br><span class="line"> std::string a = <span class="string">"114514"</span>;</span><br><span class="line"> std::string b = <span class="string">"1919810"</span>;</span><br><span class="line"> <span class="comment">// 这里的 a + b 返回的是一个临时变量,其 work 之后就不会再被用到,是天然的右值</span></span><br><span class="line"> <span class="comment">// 因此,其可以被右值引用绑定,不需要 std::move()</span></span><br><span class="line"> <span class="comment">// 注意到 work 的参数是传值,所以 work 的参数是采用右值版本的构造函数构造的</span></span><br><span class="line"> <span class="built_in">work</span>(a + b);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于那些有名字的变量例如,其都属于左值,我们需要 <code>std::move()</code> 来要强制求移动语义,调用的函数才会选择绑定右值引用的版本,否则,其只会选择绑定左值引用的重载。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">std::string a;</span><br><span class="line">std::string b;</span><br><span class="line">std::string &c = b; <span class="comment">// 正常的左值引用变量,一般来说只能绑定左值</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 右值引用变量,可以绑定那些天然的右值,将其生命周期延长至与变量一致</span></span><br><span class="line"><span class="comment">// 其当然可以绑定那些强转的右值引用,但是那些右值引用的生命周期不会延长,这么做也几乎没有意义</span></span><br><span class="line">std::string &&d = <span class="string">"ab"</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">std::string x = std::<span class="built_in">move</span>(c); <span class="comment">// 右值引用,调用移动构造,复杂度是 O(1) 的</span></span><br><span class="line">std::string y = c; <span class="comment">// 左值引用,调用拷贝构造,复杂度是 O(n) 的,n 是字符串长度</span></span><br><span class="line"></span><br><span class="line">x = std::<span class="built_in">move</span>(c); <span class="comment">// 右值引用,调用移动赋值,复杂度是 O(1) 的</span></span><br><span class="line"></span><br><span class="line">y = d; <span class="comment">// 即使是右值引用变量本身,其有名字,也是左值,调用拷贝赋值,复杂度是 O(n) 的</span></span><br><span class="line">y = std::<span class="built_in">move</span>(d); <span class="comment">// 右值引用,调用移动赋值,复杂度是 O(1) 的</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>当然,这里有一个特例,当一个变量值作为函数的返回值的时候,我们 <strong>不需要甚至不应该</strong> 用 <code>std::move</code> 来把其强制转化为右值,编译器会自动帮我们做这件事情。笔者猜测,这是因为函数中的值(变量)生命周期与函数一致,因此,其可以被看作是一个天然的右值。但是重点不在于此!如果你加了一个 <code>std::move</code> ,其有时候可能会阻止编译器进行 RVO 返回值优化 (具体是 NRVO,具名返回值优化,其优化力度强于移动语义)。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">std::string <span class="title">Hello</span><span class="params">()</span> </span>{</span><br><span class="line"> string name = <span class="string">"Hello"</span>;</span><br><span class="line"> <span class="comment">// 这里不应该用 std::move</span></span><br><span class="line"> <span class="comment">// 首先编译器会自动帮我们做这件事情(如果没有进行 NRVO 的话)</span></span><br><span class="line"> <span class="comment">// 其次这可能会阻碍 NRVO 优化</span></span><br><span class="line"> <span class="keyword">return</span> name;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function">std::string <span class="title">Hello</span><span class="params">(std::string &ref)</span> </span>{</span><br><span class="line"> <span class="comment">// 这里需要用 std::move, 是因为 ref 不是值</span></span><br><span class="line"> <span class="comment">// 其在函数结束后生命周期不一定要结束</span></span><br><span class="line"> <span class="comment">// 因此,如果需要移出数据,需要用 std::move</span></span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">move</span>(ref);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="https://s2.loli.net/2023/11/11/nScuUXxQ8aLl6vz.png" alt="NRVO 要求返回的是变量值,move 之后无法满足该要求"></p><h2 id="右值引用变量到底延长了啥"><a href="#右值引用变量到底延长了啥" class="headerlink" title="右值引用变量到底延长了啥?"></a>右值引用变量到底延长了啥?</h2><p>前文提及了右值引用变量,这东西其实表现就和一个普通的左值引用变量几乎完全一致,为了移动语义我们还需要用 <code>std::move</code> 来选择重载决议,唯一的特点就在于延长生命周期这一点……</p><p>其本质上并不是延长生命周期,而是赋予了临时变量值了一个名字…… 当其初始化等号右边是一个临时变量值 (也就是前面所说的天然右值,包括调用函数的返回值,类型转化等等) 的时候,其的确可以延长其寿命 (因为临时值的生命周期是确定的,如果没被延长立刻就死了)。但是当初始化等号右边是一个引用时,引用本身与引用的对象的生命周期并不挂钩,引用的对象可能在十万八千里外! 因此,编译器显然无法保证延长那个实际变量的周期,所以并不会有任何作为。</p><p>换句话说,只有当右值引用绑定的对象的生命周期明显确定(编译器可知,其实就是那些临时变量),其才能延长对象的寿命。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 请把 std::move 看作 static_cast <int &&></span></span><br><span class="line"><span class="type">int</span> x = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> &&tmp1 = <span class="number">0</span>; <span class="comment">// OK! 生命期延长至与 tmp1 一致</span></span><br><span class="line"><span class="type">int</span> &&tmp1 = std::<span class="built_in">move</span>(x); <span class="comment">// OK! 没有生命周期被延长</span></span><br><span class="line"><span class="type">int</span> &&tmp2 = std::<span class="built_in">move</span>(<span class="number">1</span>); <span class="comment">// 未定义行为 Undefined behavior,这是垂悬引用</span></span><br></pre></td></tr></table></figure><h2 id="移动后的对象"><a href="#移动后的对象" class="headerlink" title="移动后的对象"></a>移动后的对象</h2><p>在实际编程中,对于那些被移动的对象,其基本上只会等到自然生命周期然后被析构,我们理论上不会再对其做任何事情。但是,移动只不过说其内部的资源可以被移走,引用的对象依旧处于一个可以被析构的,合法但不确定的状态。</p><p>当我们清楚具体的实现的时候,我们依然可以操作那些被移走的变量。例如对于常见 STL 容器 <code>std::vector</code> ,在 x86_64, gcc 13.2 的标准库实现中,被移走的后 <code>std::vector</code> 为空。事实上,常见实现都是移动后设空,至少笔者遇到的 <code>std::vector</code> 和 <code>std::list</code> 和 <code>std::set</code> 什么的都是这样的。</p><p>当然,以上不是标准规定的内容,其取决于库的实现。就笔者所知,在 C++ 标准中,智能指针 和 std::thread 有特殊指定,被移动后的状态为空。如果你实在不放心,可以调用 clear() 函数后再使用 (理论上如果移动后为空,这干的是重复的事情,在 O2 下几乎肯定会被优化掉,所以不放心的话那就加上吧)。</p><p>简而言之,清楚实现的情况下,怎么玩都行(</p><h2 id="小总结"><a href="#小总结" class="headerlink" title="小总结"></a>小总结</h2><p>移动语义和右值是 C++ 搞出来的小 trick。本质上,C++ 添加了右值引用,其类似左值引用,但是其只能绑定右值,和左值引用属于不同的类型。因此,通过其提供的类型信息来选择不同的重载,从而实现了移动语义。</p><p>换句话说,其不存在所谓的修改生命周期,只不过是资源管理权的移交罢了,相关变量析构的生命周期也不会变 (唯一的特例是前面讲到的对于天然的右值,使用右值引用延长生命周期)。</p><p>其好处是在某些时候减少拷贝的开销,充分利用起来那些在析构前不会再被用到的资源,从而提高程序的效率。</p><p>特别地,当对应的函数没有重载对应的右值版本 (按值传递潜在的含有右值构造函数),或者当前类型没有可以移走的数据(比如基本类型 <code>int</code> ,移动和拷贝开销一致),那么 <code>std::move</code> 并没有任何意义。同时,对于作为函数返回值的变量值,我们不应该使用 <code>std::move</code></p><p>当然,以上的说法其实并不严谨,真正的右值还分为 xvalue ,prvalue 等等,想了解的可以自行 cppref,这里就不过多介绍了。</p><h1 id="传值还是传引用"><a href="#传值还是传引用" class="headerlink" title="传值还是传引用?"></a>传值还是传引用?</h1><p>在编写 C++ 程序的时候,一个非常头疼的问题是到底应该传值作为函数参数,还是传一个引用,如下所示。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">pass_value</span><span class="params">(std::string)</span></span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">pass_reference</span><span class="params">(<span class="type">const</span> std::string &)</span></span></span><br></pre></td></tr></table></figure><p>下面将会以一个不严谨的视角来解释如何解决这个问题。</p><h2 id="不严谨的直观"><a href="#不严谨的直观" class="headerlink" title="不严谨的直观"></a>不严谨的直观</h2><p>首先,引用的本质是什么? 在写了编译器后,我可以自信的说,在绝大多时候,引用“表现”的就和一个指针几乎完全一致,只不过它本身(即类似指针指向的地址)不能被修改。换句话说,其可以简单视作 const pointer 的语法糖。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">ref</span><span class="params">(<span class="type">int</span> &)</span></span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">ptr</span><span class="params">(<span class="type">int</span> * <span class="type">const</span>)</span></span>; <span class="comment">// 注意,const 修饰的是 * 而不是 int</span></span><br></pre></td></tr></table></figure><p>换句话说,引用类似一种指针的约定。该约定要求该指针指向的对象非空,且该指针绑定的对象(即指针的值,对象的地址)不可切换,这显然很有助于编译器做特定的优化。更重要的是,其被赋予了其他更加强大的功效,结合 C++ 语言特性。</p><p>例如,在 C/C++ 中,引用不仅能够绑定左值,对于 <code>T</code> 类型,<code>const T &</code> 还能绑定右值。而在 C++ 中只有左值可以取地址,<code>const T &</code> 这种绑定右值的特性是 <code>const T * const</code> 所不具有的。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">func</span><span class="params">()</span> </span>{ <span class="keyword">return</span> <span class="number">0</span>; }</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> x = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> &y = x; <span class="comment">// 正常情况</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// int &z = 0; 报错,因为 0 不是左值</span></span><br><span class="line"><span class="comment">// int &w = func(); 报错,函数返回值不是左值</span></span><br><span class="line"></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> &z = <span class="number">0</span>;</span><br><span class="line"><span class="type">const</span> <span class="type">int</span> &w = <span class="built_in">func</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// const int *p = &0; 报错,不能取地址</span></span><br><span class="line"><span class="comment">// const int *q = &(func()); 报错,函数返回值不是左值</span></span><br></pre></td></tr></table></figure><p>类似右值引用,const 引用绑定临时变量后,其生命周期会被延长至与引用一致。但是,对于那些强转绑定的右值引用,则不会延长。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> x = <span class="number">0</span>;</span><br><span class="line"><span class="type">const</span> <span class="type">int</span> &tmp1 = <span class="number">0</span>; <span class="comment">// OK! 生命期延长至与 tmp1 一致</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> &tmp1 = std::<span class="built_in">move</span>(x); <span class="comment">// OK! 没有生命周期被延长</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span> &tmp2 = std::<span class="built_in">move</span>(<span class="number">0</span>); <span class="comment">// 未定义行为 Undefined behavior,这是垂悬引用</span></span><br></pre></td></tr></table></figure><h2 id="开销"><a href="#开销" class="headerlink" title="开销?"></a>开销?</h2><p>既然和指针类似,那么其开销也应该和指针类似,而指针本身在 64 位系统上就占据 8 个字节。显然,比起恐怖、复杂度未知的拷贝,这个开销是可以接受的。这也是为什么很多人都推荐初学者使用引用而不是指针。</p><p>但是,如果拷贝的开销是已知的,那么我们就权衡一下两者的开销比。对于那些短类型例如 <code>int</code> 和 <code>long long</code> 之类,我们完全没必要传引用。拷贝的开销此时和引用一样,还可以避免引用(指针)间接取值的潜在开销 (这需要一次额外的不确定的地址访问,对比之下局部变量可能被优化为寄存器存储,或者是一个确定的栈上地址)。</p><p>因此,笔者的建议是,对于那些拷贝不超过 16 Byte 的类型,也就是不超过<code>sizeof(std::size_t) * 2</code> 大小的类型,我们应该传值而不是传引用,除了基本类型,常见的有 <code>std::complex <double></code>,<code>std::string_view</code>,<code>std::pair <int, int></code> ,甚至是 <code>std::unique_ptr</code> 等等 (这是因为 <code>unique_ptr</code> 只支持移动不支持拷贝,移动构造只需要拷贝一个指针的大小)。当然,这个 16 Byte 的界限并不是绝对的,只是一个经验值,可以根据实际情况进行调整。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">good_func</span><span class="params">(<span class="type">const</span> std::string &,<span class="type">int</span>)</span></span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">good_iff_you_know_wtf_u_r_doing</span><span class="params">(std::string,<span class="type">const</span> <span class="type">int</span> &)</span></span>;</span><br></pre></td></tr></table></figure><h1 id="new-和-delete-到底干了什么"><a href="#new-和-delete-到底干了什么" class="headerlink" title="new 和 delete 到底干了什么"></a>new 和 delete 到底干了什么</h1><p>大家都知道 <code>C</code> 语言中 <code>malloc</code> 和 <code>free</code> 分别申请内存/释放内存,而 <code>C++</code> 中,我们一般使用 <code>new</code> 和 <code>delete</code> 来申请/释放内存。但是,这两者到底干了什么呢?</p><h2 id="new"><a href="#new" class="headerlink" title="new"></a>new</h2><p>由于 C++ 的特性,在使用 <code>new</code> 的时候不仅分配了内存,还会调用构造函数来构造对象。</p><p>在分配内存的时候,其实 <code>new</code> 差不多就是调用了 <code>malloc</code> 来实现的,但是当空间不够的时候,<code>malloc</code> 会返回空指针,而 <code>new</code> 会抛出异常。如果你不希望抛出异常,可以使用 <code>nothrow</code> 版本的 <code>new</code> ,其会返回空指针。例子来自 <a href="https://en.cppreference.com/w/cpp/memory/new/nothrow">cppreference</a> 。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><new></span></span></span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="keyword">new</span> <span class="type">int</span>[<span class="number">100000000ul</span>]; <span class="comment">// throwing overload</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="built_in">catch</span> (<span class="type">const</span> std::bad_alloc& e) {</span><br><span class="line"> std::cout << e.<span class="built_in">what</span>() << <span class="string">'\n'</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="type">int</span>* p = <span class="built_in">new</span>(std::nothrow) <span class="type">int</span>[<span class="number">100000000ul</span>]; <span class="comment">// non-throwing overload</span></span><br><span class="line"> <span class="keyword">if</span> (p == <span class="literal">nullptr</span>) {</span><br><span class="line"> std::cout << <span class="string">"Allocation returned nullptr\n"</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>特别地,对于那些基本的内置类型,比如 <code>int</code> 和 <code>char *</code> 这些,其没有所谓构造函数因此,包括类似地的聚合类,比如 <code>struct { int x,y; };</code> 。此时,单纯的 <code>new</code> 不会调用构造函数,而是直接分配内存。这种情况下,如果你想要让里面的数据默认初始化(为 0),你需要用花括号或括号显式初始化。</p><p>事实上,该初始化过程非常类似 <strong>局部变量的初始化</strong> 。对于内置类型,在构造函数未指值的时候 (比如没有构造函数的 <code>int</code>,或者构造函数没为这个 <code>int</code> 成员变量初始化),其值是不确定的。一般来说,对于聚合类/内置类型,会使用 <code>{}</code> 初始化来保证是 <code>0</code> 。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> *p = <span class="keyword">new</span> <span class="type">int</span>; <span class="comment">// 分配内存,但是不会调用构造函数</span></span><br><span class="line"><span class="type">int</span> *q = <span class="keyword">new</span> <span class="built_in">int</span>(); <span class="comment">// 分配内存,同时调用构造函数,初始化为 0</span></span><br><span class="line"><span class="type">int</span> *r = <span class="keyword">new</span> <span class="type">int</span>{}; <span class="comment">// 同上</span></span><br><span class="line">std::cout << *p << std::endl; <span class="comment">// 未定义行为,p 指向的内存未初始化</span></span><br><span class="line">std::cout << *q << std::endl; <span class="comment">// 正常输出 0</span></span><br><span class="line">std::cout << *r << std::endl; <span class="comment">// 正常输出 0</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">A</span> { <span class="type">void</span> *p; <span class="type">long</span> <span class="type">long</span> x; };</span><br><span class="line">A *a = <span class="keyword">new</span> A; <span class="comment">// 分配内存,什么都不干</span></span><br><span class="line">A *b = <span class="keyword">new</span> A{}; <span class="comment">// 分配内存,初始化为 0</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">B</span> { <span class="type">int</span> x; <span class="built_in">B</span>() = <span class="keyword">default</span>; }; <span class="comment">// 没有初始化哦</span></span><br><span class="line"></span><br><span class="line">B *c = <span class="keyword">new</span> B; <span class="comment">// 分配内存,什么都不干</span></span><br><span class="line">B *d = <span class="keyword">new</span> <span class="built_in">B</span>(); <span class="comment">// 分配内存,调用构造函数,初始化为 0</span></span><br></pre></td></tr></table></figure><p>那为什么不要求基本类型默认初始化为 <code>0</code> 而要求显式写出 <code>{}</code> 或 <code>()</code> 才能呢?这是因为潜在的开销!<code>C++</code> 的核心理念是抽象无开销 (Zero overhead) 。那么,初始化为 <code>0</code> 还是额外的多做了点事情的对吧,这点开销很有可能是能够避免的,特别是对于大数组的初始化。</p><h2 id="其他的-new"><a href="#其他的-new" class="headerlink" title="其他的 new"></a>其他的 new</h2><p>除了常用的 <code>new</code> 加类型来申请内存,还有一些其他的 <code>new</code> ,比如 <code>new[]</code> 和 <code>new (std::nothrow)</code> 等等。当然,这些主题逻辑上是差不多的,都是先分配内存,后尝试调用默认构造函数。真正有意思的是 <code>operator new</code> 和 <code>placement new</code> 。简单来说,常见的 <code>new</code> = <code>operator new</code> + <code>placement new</code> 。</p><p><code>operator new</code> 大致声明如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> * <span class="keyword">operator</span> <span class="title">new</span><span class="params">(<span class="type">size_t</span> size)</span></span>;</span><br></pre></td></tr></table></figure><p>看到 <code>void *</code>,想必很多熟悉 C 语言的高手就明白了。这东西几乎就是一个 C 语言的 <code>malloc</code> ,只不过额外多了一个防止内存不够的异常抛出。当然,这个 <code>operator new</code> 是可以被重载的,我们可以自己实现一个 <code>operator new</code> 来实现自己的内存分配策略。网上教程也很多,这里就不展开了,多看 cppreference 就好。</p><p><code>placement new</code> 其实根本都称不上 <code>new</code> ,其作用是在给定的指针指向的空间上调用构造函数。举例如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> *buf = <span class="keyword">operator</span> <span class="built_in">new</span>(<span class="built_in">sizeof</span>(std::string)); <span class="comment">// raw memory</span></span><br><span class="line">std::string *p = ::<span class="built_in">new</span> (buf) std::<span class="built_in">string</span>(<span class="string">"Hello"</span>); <span class="comment">// placement new</span></span><br><span class="line"></span><br><span class="line">std::cout << *p << std::endl;</span><br></pre></td></tr></table></figure><p>事实上,给定的指针指向的空间不一定要是 <code>new</code> 得到的,甚至可以是栈上的空间。比如 <code>char</code> 数组构成的栈上缓冲区之类。</p><h2 id="delete"><a href="#delete" class="headerlink" title="delete"></a>delete</h2><p>与之相对的,<code>delete</code> 也不仅仅是 <code>free</code> 这么简单,其显然还会额外地调用析构函数,这是 <code>C++</code> 面向对象的特点。当然,对于基本类型,或者简单类型 (比如 <code>struct {int x,int y}</code>) ,其只有 <code>trivial destructor</code> ,也就是什么都不干的析构函数,此时 <code>delete</code> 也不会额外多做什么。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> *p = <span class="keyword">new</span> <span class="type">int</span>;</span><br><span class="line"><span class="keyword">delete</span> p; <span class="comment">// 析构函数什么都不干,直接 free</span></span><br><span class="line"></span><br><span class="line">std::string *q = <span class="keyword">new</span> std::string {};</span><br><span class="line"><span class="keyword">delete</span> q; <span class="comment">// 析构函数会调用 std::string 的析构函数,然后 free</span></span><br></pre></td></tr></table></figure><p>特别地,如果 <code>delete</code> 或 <code>free</code> 的指针是空指针,那么其什么都不会做!!! 换句话说,这是安全的,你不应该在这之前判断指针是否为空。这是一个常见的习惯,和 <code>new</code> 之后判断是否为空指针一样 (因为前面说了,<code>new</code> 会抛出异常,只有 <code>nothrow</code> 版本的 <code>new</code> 才会返回空指针),都是多余的。</p><p>如果你是用 <code>placement new</code> 在栈上的空间放置的内存。那么我们肯定不能调用 <code>delete</code> 来释放内存,因为栈上内存不能 <code>free</code> 。对应的,我们必须对指针显式地调用析构函数。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> std::string;</span><br><span class="line"><span class="type">char</span> buf[<span class="built_in">sizeof</span>(std::string)];</span><br><span class="line">string *p = ::<span class="built_in">new</span> (buf) <span class="built_in">string</span>(<span class="string">"Hello"</span>);</span><br><span class="line">p->~<span class="built_in">string</span>(); <span class="comment">// 显式调用析构函数</span></span><br></pre></td></tr></table></figure><p>这时候,存在一个小小的 bug ,那就是对于基本类型,其没有析构函数,显然我们不能显式调用析构函数,比如 <code>~int()</code> 是错误的。但是当其作为模板参数,在模板实例化的时候被替换,那么此时是合法的 (当然,啥都不会干)。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 模板类同理,这里就举一个模板函数的例子</span></span><br><span class="line"><span class="comment">// 即使传入参数为 int,其也可以过编译</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">typename</span> _Tp></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">foo</span><span class="params">(_Tp *p)</span> </span>{ p->~_Tp(); }</span><br></pre></td></tr></table></figure><h2 id="省流"><a href="#省流" class="headerlink" title="省流"></a>省流</h2><p>简单来说,<code>new</code> 和 <code>delete</code> 其实就是对于 <code>malloc</code> 和 <code>free</code> 的包装。由于 C++ 独特的构造函数和析构函数,对于一个对象,其在申请内存之后必须先执行构造函数,而在归还内存前必须执行析构函数。经过 <code>new</code> 和 <code>delete</code> 和的包装,我们就不用手动对于申请的内存调用构造函数 (即 <code>placement new</code>,如果有的话),以及在归还内存前调用析构函数 (如果有的话)。</p><p>稍微总结一下,<code>new</code> 的行为大致是这样的:</p><ol><li>首先分配内存空间。如果不够就抛出异常,如果使用 <code>nothrow</code> 版本的 <code>new</code> 则返回空指针。</li><li>尝试调用构造函数。如果是没有构造函数的类型,会调用默认的构造函数。对于 (默认) 构造函数没有覆盖的内置类型,需要用 <code>{}</code> 或 <code>()</code> 显式初始化为 <code>0</code> ,否则其值不确定。</li></ol><p>而 <code>delete</code> 的行为大致是这样的:</p><ol><li>首先调用析构函数。如果是空指针或 <code>trivial destructor</code>,什么都不干。</li><li>归还内存空间。如果是空指针,什么都不干。</li></ol><p>这其实也就是 C++ 的内存模型,核心在于构造函数和析构函数在合适的时候调用。</p><p>对于你不清楚的类型实现,请务必保证在一块空间上,只调用一个类型的构造函数。在使用的时候,请务必该对象执行过构造函数。在归还内存前或离开作用域的时候,请务必保证该对象执行过析构函数。只有构造过的对象才是 “存活的” ,而析构前的对象必须是 “存活的” ,析构后对象必须是 “死亡的” ,当然空间也就可以继续使用或者归还了。</p><p>当然,对于大家熟知的简单类型比如 <code>int</code>,自然没那么多讲究。只要清楚原理,理论上怎么玩都可以 (毕竟指定编译器版本和操作系统,给定对象的实现,行为显然是定义的)。</p>]]></content>
<summary type="html">编译器写的。</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="优化" scheme="http://darksharpness.github.io/categories/C/%E4%BC%98%E5%8C%96/"/>
<category term="优化" scheme="http://darksharpness.github.io/tags/%E4%BC%98%E5%8C%96/"/>
<category term="C++" scheme="http://darksharpness.github.io/tags/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>Compiler for Mx* 编译器随笔</title>
<link href="http://darksharpness.github.io/CompilerMx/"/>
<id>http://darksharpness.github.io/CompilerMx/</id>
<published>2023-09-29T04:48:43.000Z</published>
<updated>2024-03-02T08:11:28.000Z</updated>
<content type="html"><![CDATA[<p>注: 本文于 2024-03-02 更新循环相关优化部分,其他部分稍作修改。</p><p>课程要求,写了一个简单语言 Mx* 的编译器,语法规则<a href="https://github.com/ACMClassCourses/Compiler-Design-Implementation">请点击这里</a> 。笔者写的很烂,而且项目还烂尾了,所以不放自己写的链接了。</p><p>其简单来说是一个残疾版 Java + C ,拥有类似 Java 的对象模型,所有 class 都是引用类型 (笔者汇编实现其实就是指针),且有最基本的构造函数和成员函数 ,但没有复杂的面向对象特性 (比如继承,虚函数等等) ,甚至连常见关键词如 static 什么的都没有。当然,解决一些简单的问题还是绰绰有余的。</p><p>编译器最后生成汇编代码的目标平台是 RISC-V 32bit, Integer Extended , 测评使用的是 <a href="https://github.com/Engineev/ravel">ravel 模拟器</a> <del>不过bug(feature)还不少</del> 。</p><h1 id="AST"><a href="#AST" class="headerlink" title="AST"></a>AST</h1><p>笔者前端使用的是 antlr ,通过自己编写 g4 实现 Lexer/Parser 的功能 ,基本属于是自动生成。这部分个人感觉难度不高,除了语法检查有很多细枝末节要考虑,其他基本没啥值得讲的。不过其实在这一部分,其实已经有可以优化的空间了,例如对于无副作用的一些恒等表达式,以及无用的数组 new 。当然,由于笔者实在是没空,在 AST 上我没有做任何优化。</p><h1 id="IR"><a href="#IR" class="headerlink" title="IR"></a>IR</h1><p>IR 上的优化可以说是编译器优化的核心。可以说我 90% 的优化都是作用在 IR 上的。</p><p>笔者的 IR 采用的是简化版本的 llvm IR (至少生成的.ll 可以用 clang 编译运行且不会出错)。下面将会简单讲讲笔者在 IR 上具体写了那些玄学优化,以及计划(但实际没写)的那些优化。</p><h2 id="mem2reg-SSA"><a href="#mem2reg-SSA" class="headerlink" title="mem2reg-SSA"></a>mem2reg-SSA</h2><p>前置知识: <a href="https://oi-wiki.org/graph/dominator-tree/">支配树</a> 。</p><p>SSA(Static single assign) 是指满足虚拟寄存器只会被被单一赋值的 IR ,在 SSA 上,许多的优化可以被简化,且时间复杂度会更优。显然,内存是不能也不需要保证 SSA 的。而局部变量,其可能被多次赋值,不一定能满足 SSA 的条件,所以在生成 IR 的时候一般会用 alloc 申请栈空间用于其值的存储。</p><p>但是实际上,很多时候,寄存器是充足的,我们期望可以把局部变量的值放在寄存器(在 IR 中呈现为虚拟寄存器) 里面,从而避免了高开销的内存读写操作。但是前面也说了,局部变量可以被重复赋值,比如在不同分支中有不同的取值,导致不一定能放入虚拟寄存器。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">func</span><span class="params">(<span class="type">bool</span> cond)</span> </span>{</span><br><span class="line"> <span class="type">int</span> z;</span><br><span class="line"> <span class="keyword">if</span>(cond) {</span><br><span class="line"> z = <span class="number">1</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> z = <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> z;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是,mem2reg 为我们提供了一种消除 SSA 形式 IR 中部分 alloc 的方法。其思路大致如下:</p><p>我们把每条指令看作图中的一个节点。除了分支指令有两个后继 (即出边),其他指令有唯一后继。如果一个局部变量 x 在一条指令的位置被赋值,那么在这条指令所支配 (此处指的是支配树的支配关系) 的范围内,在下一次被赋值之前,其值不会改变,这是由支配树的性质所保证的。但是在其支配边界上,到达该位置的 x 可能就不止来自这个方向的赋值。还可能存在其他方向的赋值,这也很符合支配 “边界” 的直观。为了保险起见,我们要根据其来的方向,为这个 “边界” 上的指令进行值合并。在这里,llvm IR 为我们提供了一个工具函数: phi 函数。其不是真正的函数调用(call) 指令,其作用是根据跳转的分支为一个变量赋值。例如,对于上面那段 C++ 代码,其可以生成类似如下的 llvm IR 代码:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">entry:</span><br><span class="line"> br i1 %cond-0, label %if-0-true-0, label %if-0-false-0</span><br><span class="line"></span><br><span class="line">if-0-true-0:</span><br><span class="line"> br label %if-0-end</span><br><span class="line"></span><br><span class="line">if-0-false-0:</span><br><span class="line"> br label %if-0-end</span><br><span class="line"></span><br><span class="line">if-0-end:</span><br><span class="line"> %z-1.mem.0 = phi i32 [ 1 , %if-0-true-0 ] , [ 2 , %if-0-false-0 ]</span><br><span class="line"> ret i32 %z-1.mem.0</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>简而言之,phi 函数可以用来合并来自不同分支的赋值,类似维护了变量的不同版本,从而保证了 SSA 的形式。这也带来了 mem2reg 这一优化,其可以把所有没有被取地址,且不是 volatile 的局部变量转化为虚拟寄存器,进而消除相关的 load/store。由于 Mx* 的语言特性,其天然不存在取地址,因此所有天然的 alloca 理论上都能被消除,</p><p>mem2reg 首先建立一个函数的控制流图,然后对于所有的局部变量 (alloca 产生),对每个对这个局部变量的赋值 (目前只含 store 指令),我们在其支配边界标记插入关于该变量的 phi 函数 (注意,phi 函数也是关于这个变量的赋值。我的解决方案是先处理原来的 store,第二遍再处理 phi 产生的赋值)。</p><p>通过扫描,我们容易确定每个块结束后,一个局部变量在当前块结束时候的值。对于每个新插入的 phi 以及其对应的原本的局部变量 x ,我们只需去每个前驱块或许该局部变量的值并且填入 phi 即可。</p><p>当然,既然讲到了 phi 函数,就需要特别地说明一下,phi 函数的赋值是并行进行的,即一个块里面所有 phi 同时赋值,例如下面的 两个 phi 语句会交换 x 和 y 的值 (当从 %BB1 跳过来的时候):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">%x = phi i32 [ 0 , %entry ] , [ %y , %BB1 ]</span><br><span class="line">%y = phi i32 [ 1 , %entry ] , [ %y , %BB1 ]</span><br></pre></td></tr></table></figure><p>在后文的关于 IR 优化讨论中,我们都假定是在 SSA 形式的 IR 上进行。</p><h2 id="DCE-amp-ADCE"><a href="#DCE-amp-ADCE" class="headerlink" title="DCE&ADCE"></a>DCE&ADCE</h2><p>死代码消除是一个非常常见的优化。在程序中,难免会出现一些无效的死代码。事实上,在上一步 mem2reg 之后,由于其保守地在每个支配边界都会插入 phi,这可能会导致出现一些无效的 phi (即结果在后面用不到,但是由于 mem2reg 维护了变量在每个块的不同版本,保证正确性,因此插入了不必要的 phi)。这时候,死代码消除就能很好的简化生成的 IR 。</p><p>死代码消除有很多种,笔者想出的一个最基础的版本大致如下:</p><ul><li>没被标记为副作用的指令都是无用的。</li><li>有副作用的指令用到的变量,其定值(因为 SSA ,所以有且仅有一次定值(即被定义)) 语句是有副作用的。</li><li>内置的 IO 函数 / 存在全局影响的函数 是有副作用的</li><li>store 指令认为是有副作用的</li><li>branch 指令认为是有副作用的</li><li>return 指令认为有副作用的</li></ul><p>通过沿着 def-use 链条前向传播 (依赖 SSA!),我们可以在线性时间内确定所有的活指令,从而删除死指令。这个死代码消除相当的 naive ,也不能消除空循环这种无效代码,不过好处是其跑的非常快,只需要线性的时间很小的常数就能跑完,且不会修改控制流图,因此被我用来作为其他所有优化结束后顺便跑掉的一个 pass 。</p><p>真正的死代码消除,应该能够识别那些没有副作用的分支,从而把那些无效分支清除干净 (包括死循环) 。事实上,基础版本的死代码优化对于分支估计还是过于保守。ADCE (Aggressive Dead Code Elimination) ,可以通过控制流图分析来确定那些有效的分支,不过需要的是反向流图的支配关系。过程大致如下:</p><ul><li>所有基本块初始认为是死的</li><li>有副作用的指令的块都是活块</li><li>内置的 IO 函数 / 存在全局影响的函数 是有副作用的</li><li>store 指令认为是有副作用的</li><li>return 指令认为是有副作用的</li><li>jump 指令到活块认为是有副作用</li><li>branch 指令,只有当其处于某个活块的反向支配边界上,其才是存活的。</li></ul><p>ADCE 比起 DCE,虽然依赖反向流图的支配关系 (这个东西的建立特别费时) ,但是可以消除那些无效的分支 (例如空循环或无效循环),的确对的上 “Aggressive” 这个名字。</p><h2 id="SCCP"><a href="#SCCP" class="headerlink" title="SCCP"></a>SCCP</h2><p>常量传播也是非常常见的一个优化。其<del>降低程序员的心智负担,让其大胆写出更多烂代码</del>主要是优化一些表示常数的变量(其实就是临时寄存器)<del>所以为什么不引入 const</del> ,将其在使用处直接替换为对应的常数,这便是最基本的常量传播。</p><p>当然,如果两个 SSA 的变量表示相同的数值 (例如 x = y + 0,显然 x = y),那么显然我们也可以把 x 在所有使用处替换为 y 。注意,其正确性其实并不 trivial。SSA 要求每个变量只能在其支配的基本块内出现,x 支配的块 y 也一定支配,这并不 trivial 。但是注意到 x = y + 0 ,说明 y 必然支配 x 所在位置,因此 x 所支配的块 y 也必然支配 (看看支配的定义即可)。</p><p>对于分支和 phi 函数,常量传播依然可以进行下去: 我们先从入口出发。如果遇到 phi 函数,我们先按照来的方向给赋值,如果之后从别的方向来并产生了矛盾,那么再标记这个值不是常量 (或者其他固定值) 。如果遇到一个分支,显然,branch 语句的 condition 的值肯定已经确定是常量或者不是常量。如果是常量,我们就根据 condition 走对应的分支。如果不是常量,我们只能假定两个分支都会走。</p><p>因此,常量传播流程大致如下:</p><p>首先标记所有的变量 (临时寄存器) 的状态为未知。变量状态有三种: 未知(Unknown) , 已知 (Known,必须是常量或者其他固定值),非常量 (Non-const)。状态合并规则如下:</p><ul><li>Unknown + any = any.</li><li>Non-const + any = Non-const</li><li>Known_i + Known_i = Known_i</li><li>Known_i + Known_j = Non-const (i,j 不同)</li></ul><p>然后,我们从入口出发(把入口加入块的工作列表 work_list_1)。</p><p>对于 work_list_1 的块,我们按顺序遍历当前工作的块。对于 jump 指令,我们将其目标块加入 work_list_1。对于 branch ,按照之前所讲处理即可。对于其他语句,我们正常进行赋值,并且将 赋值结果 与 结果变量的当前状态 进行合并。如果发现合并结果发生改变,那么我们把这个变量所有被使用的地方(指令)加入 work_list_2 (指令工作列表)。</p><p>对于 work_list_2 的指令,我们尝试对其重新计算,并且将 赋值结果 与 结果变量的当前状态 进行合并。如果发生改变,那么类似地,那么我们把这个变量所有被使用的地方(指令)加入 work_list_2 。</p><p>我们一直执行,直到两个 work_list 都被清空。由于状态合并最多改变两次 (Unknown->known->non_const 的过程是单向不可逆),所以不用担心该操作的复杂度爆炸。不过要特别注意的是,我们沿着某一条边进入一个块,遍历一个块的所有语句,这个操作只会执行至多一次,因为后面如果产生了更新,那么肯定会通过 work_list_2 更新过来,所以不用再 visit 一遍。而同时,一个块除了 phi 语句之外的其他语句至多只需要 visit 一遍,在第二次从 work_list_1 取出的时候,只需要重新 visit 那些 phi 函数就行了。如果存在依赖的改变,那么自然会从 work_list_2 更新过来;如果不存在依赖 (比如两个常数的和) ,那么第一次 visit 的时候的结果就自然是正确的了。</p><p>这部分的证明还是不难但也不 trivial 的,建议读者自行思考各种 corner case 下的正确性。如果有任何疑问,可以参考<a href="https://dl.acm.org/doi/pdf/10.1145/103135.103136">原始论文</a>。(不过主体部分还是很好想到的,事实上笔者一开始基本就是按照自己的理解搓了一个几乎差不多的 SCCP,不过论文还考虑了各种优化,包括哪些只 visit 一次,的确人类智慧)。</p><h2 id="CFG"><a href="#CFG" class="headerlink" title="CFG"></a>CFG</h2><p>流图化简,即 CFG 上的化简,是一个完全由我自己构思的优化 (其实是没看到类似的论文 <del>其实还是懒得找</del> )。其可以尽可能地消除无效的 jump ,同时消除部分 phi 语句 (不过笔者还没 100% 实现)。</p><h3 id="Jump-Elimination"><a href="#Jump-Elimination" class="headerlink" title="Jump-Elimination"></a>Jump-Elimination</h3><p>在前面这几个不痛不痒的小优化之后,我们会发现出现了很多基本块的体积减少了一点,很多甚至只剩下一个 jump 语句。在极端的情况下,我们可以看到一堆由连续重复 jump 指令,例如:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">BB0:</span><br><span class="line"> br label BB4</span><br><span class="line">BB1:</span><br><span class="line"> br label BB2</span><br><span class="line">BB2:</span><br><span class="line"> br label BB3</span><br><span class="line">BB3:</span><br><span class="line"> br label BB4</span><br><span class="line">BB4:</span><br><span class="line"> br label exit</span><br><span class="line">exit:</span><br><span class="line"> ret void</span><br></pre></td></tr></table></figure><p>事实上,这些无效的 jump 都可以被直接压缩为一条语句: <code>ret void</code> 。你可能觉得这不过是常数级别的优化。的确,jump 的确太快了,这些优化显得略有点没用。但是如果 jump 多达几百个呢,这好像就不仅仅是常数了吧。</p><h3 id="Condition-Phi-Elimination"><a href="#Condition-Phi-Elimination" class="headerlink" title="Condition-Phi-Elimination"></a>Condition-Phi-Elimination</h3><p>考虑如下语句:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">BB2:</span><br><span class="line"> %cond = phi i1 [ false , %BB0 ] , [ true , %BB1 ]</span><br><span class="line"> br i1 %cond BB2, label %BB3, label %BB4</span><br></pre></td></tr></table></figure><p>如果从 %BB0 过来,那么显然只会跳到 %BB4 。如果 %cond 只在当前条件语句被使用到,那么我们甚至可以直接消除这个 %cond 变量,将这个条件分支直接压缩没了。要知道条件分支对于现代 CPU 的流水线可是一个很糟糕的东西,分支预测错误可是会带来流水线的中断等一系列后果,其速度很慢。因此,我们可以考虑把条件分支顺便也压缩了。</p><h3 id="Tail-Phi-Elimination"><a href="#Tail-Phi-Elimination" class="headerlink" title="Tail-Phi-Elimination"></a>Tail-Phi-Elimination</h3><p>特别地,如果一个 phi 语句后面紧跟 ret 语句,那么显然其与 ret 的返回值相关(否则,之前的死代码消除将会干碎这个无效 phi)。那么,我们可以把当前块拆开,不同块跳过来的时候直接 return 不同的值。这个操作几乎无法消除多少 phi ,看起来没啥大用处,但是其可以为尾递归优化留下空间。考虑以下的 C++ 代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">tail</span><span class="params">(<span class="type">int</span> n)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> n < <span class="number">0</span> ? n : <span class="built_in">tail</span>(n - <span class="number">10</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在正常的 IR 生成中,我们会用 phi 来合并三目运算符的结果。但是这并不利于尾递归优化。当我们手动拆开 phi,相当于将原来的 C++ 代码拆成如下形式:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">tail</span><span class="params">(<span class="type">int</span> n)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (n < <span class="number">0</span>) <span class="keyword">return</span> n;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">return</span> <span class="built_in">tail</span>(n - <span class="number">10</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>显然,这种形式的代码看起来就可以尾递归优化 (甚至是再把尾递归优化为循环)。这也是这个优化的另一个好处。</p><h3 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h3><p>以上就是我个人想到的 CFG 上几处小优化。Jump-Elimination 可用于清除并简化其他优化之后复杂的流图,Condition-Phi-Elimination 则对于 复杂短路逻辑表达式 的优化有奇效,Tail-Phi-Elimination 则能帮助尾递归优化。</p><h2 id="Local-optimization"><a href="#Local-optimization" class="headerlink" title="Local-optimization"></a>Local-optimization</h2><p>这里面涉及了很多块内的优化。这些优化不需要复杂的控制流图,只需要逐块分析即可,不过全是人类智慧。当然,该优化其实可以 global 化,但是由于笔者实在是太累了,所以暂时只写了局部的版本。</p><h3 id="Arithmetic-Simplification"><a href="#Arithmetic-Simplification" class="headerlink" title="Arithmetic-Simplification"></a>Arithmetic-Simplification</h3><p>算术化简是最常见的一个优化了。在 IR 层级,能做的算术化简其实已经所剩无几了。换句话说,该优化其实本应该在 AST 就做掉不少,至少笔者是这么认为的。</p><p>尽管 IR 上处处受限,但是我们依然可以发现以下这些比较 trivial 的表达式优化 (以下摘自笔者的破烂代码的注释):</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Swap operands if the left one is a constant</span></span><br><span class="line"><span class="comment"> * for those symmetric operators: + * & | ^ </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Strength reduction and replacement:</span></span><br><span class="line"><span class="comment"> * X - C --> X + (-C)</span></span><br><span class="line"><span class="comment"> * X + X --> X << 1</span></span><br><span class="line"><span class="comment"> * X * pow(2,n) --> X << n</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Negative elimination rule:</span></span><br><span class="line"><span class="comment"> * 0 - Y --> (-Y)</span></span><br><span class="line"><span class="comment"> * X + (-Y) --> X - Y</span></span><br><span class="line"><span class="comment"> * (-Y) + X --> X - Y</span></span><br><span class="line"><span class="comment"> * X - (-Y) --> X + Y</span></span><br><span class="line"><span class="comment"> * (-X) * C --> X * (-C) // iff non-power-of-2 C</span></span><br><span class="line"><span class="comment"> * (-X) * (-Y) --> X * Y</span></span><br><span class="line"><span class="comment"> * (-X) / C --> X / (-C)</span></span><br><span class="line"><span class="comment"> * C / (-X) --> (-C) / X</span></span><br><span class="line"><span class="comment"> * (-X) / (-Y) --> X / Y</span></span><br><span class="line"><span class="comment"> * X % (-Y) --> X % Y</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Merge operators to try generate deadcode (to be removed~):</span></span><br><span class="line"><span class="comment"> * Bitwise:</span></span><br><span class="line"><span class="comment"> * (X & C1) & C2 --> X & (C1 & C2)</span></span><br><span class="line"><span class="comment"> * (X | C1) | C2 --> X | (C1 | C2)</span></span><br><span class="line"><span class="comment"> * (X ^ C1) ^ C2 --> X ^ (C1 ^ C2)</span></span><br><span class="line"><span class="comment"> * Add or Sub:</span></span><br><span class="line"><span class="comment"> * (X + C1) + C2 --> X + (C1 + C2)</span></span><br><span class="line"><span class="comment"> * (X - C1) + C2 --> X + (C2 - C1)</span></span><br><span class="line"><span class="comment"> * Mult or Div:</span></span><br><span class="line"><span class="comment"> * (X << C1) * C2 --> X * (C2 << C1)</span></span><br><span class="line"><span class="comment"> * (X * C1) * C2 --> X * (C1 * C2)</span></span><br><span class="line"><span class="comment"> * (X * C1) / C2 --> X * (C1 / C2) // iff C2 divides C1</span></span><br><span class="line"><span class="comment"> * (X * C1) % C2 --> 0 + 0 = 0 // iff C2 divides C1</span></span><br><span class="line"><span class="comment"> * (X << C1) % C2 --> 0 + 0 = 0 // iff C2 divides pow(2,C1)</span></span><br><span class="line"><span class="comment"> * Shift:</span></span><br><span class="line"><span class="comment"> * (X << C1) << C2 --> X << (C1 + C2)</span></span><br><span class="line"><span class="comment"> * (X >> C1) >> C2 --> X >> (C1 + C2)</span></span><br><span class="line"><span class="comment"> * (X << C1) >> C2 --> X << (C1 - C2) or X >> (C2 - C1)</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Special case in merging operators.</span></span><br><span class="line"><span class="comment"> * (C1 - X) + C2 --> (C1 + C2) - X</span></span><br><span class="line"><span class="comment"> * C1 - (X + C2) --> (C1 - C2) - X</span></span><br><span class="line"><span class="comment"> * C1 - (C2 - X) --> (C1 - C2) + X</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Maybe I will write this: (Actually not).</span></span><br><span class="line"><span class="comment"> * (0 - X) / X --> 0 + -1 = -1</span></span><br><span class="line"><span class="comment"> * (0 - X) % X --> 0 + 0 = 0</span></span><br><span class="line"><span class="comment"> * X / (0 - X) --> 0 + -1 = -1</span></span><br><span class="line"><span class="comment"> * X % (0 - X) --> 0 + 0 = 0</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Special case for non-constant test:</span></span><br><span class="line"><span class="comment"> * (X ^ Y) ^ X --> Y + 0 = Y</span></span><br><span class="line"><span class="comment"> * (X | Y) & X --> X + 0 = X</span></span><br><span class="line"><span class="comment"> * (X & Y) | X --> X + 0 = X</span></span><br><span class="line"><span class="comment"> * (X | Y) | X --> X | Y</span></span><br><span class="line"><span class="comment"> * (X & Y) & X --> X & Y</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * (X - Y) + Y --> X + 0 = X</span></span><br><span class="line"><span class="comment"> * (X + Y) - X --> Y + 0 = Y</span></span><br><span class="line"><span class="comment"> * (X * Y) / X --> Y + 0 = Y</span></span><br><span class="line"><span class="comment"> * (X * Y) % X --> 0 + 0 = 0</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * Negative generation rule:</span></span><br><span class="line"><span class="comment"> * 0 - X --> (-X)</span></span><br><span class="line"><span class="comment"> * X * (-1) --> (-X)</span></span><br><span class="line"><span class="comment"> * X / (-1) --> (-X)</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><p>这些优化单独来看几乎没啥用,但是结合其他的会有奇效。</p><h3 id="CSE"><a href="#CSE" class="headerlink" title="CSE"></a>CSE</h3><p>公共子表达式消除是一个常见的优化。对于公共子表达式,我们可以用前者在后者使用处替换。但是,这并不一定是好事情。因为如果这个计算过程是非常廉价的,且计算结果的寿命并不长,那么你重复使用第一次的计算结果,可能会导致一个无效的寄存器占用,甚至不如每次都重新计算。不过由于是在块内,所以一般来说还是 ok 的,不会带来过多的副作用。</p><h3 id="UB-elimination"><a href="#UB-elimination" class="headerlink" title="UB-elimination"></a>UB-elimination</h3><p>在代码优化的过程中,我们可能会遇到一些含有未定义行为的语句。例如: 整数除以 0 ,一个没有控制流的基本块 (常见于函数无返回值),读写空指针…… 由于我们可以假定程序是正确的,因此我们可以直接消除这些未定义行为的语句,但这是远远不够的。事实上,到达这些基本块的途径都应当是非法的 (因为假定没有未定义行为),因此,我们可以把这个基本块从控制流图中直接移除。</p><p>具体流程大致如下: 首先标记所有含 UB 的块为不可达。然后直接分析控制流图,其中入口为第一个基本块,出口为所有含 return 的基本块。我们分别从出口/入口进行 bfs/dfs 。只有当一个块可以从入口到达,并且可以从出口到达,我们才认为这个块是可达的。最后,我们把所有不可达的块从控制流图中移除。</p><p>特别地,对于那些跳往不可达块的分支,需要把分支转化为无条件跳转。同时,对于 phi 节点中不可达块跳过来的那个格子,其也需要被清空。简而言之,注意下细节即可。</p><h3 id="Load-Store-tracing"><a href="#Load-Store-tracing" class="headerlink" title="Load/Store tracing"></a>Load/Store tracing</h3><p>这个技术其实理论上需要用到更加复杂内存追踪技术,例如 equalSet 什么的,但是笔者实在是太忙了,所以没写这么多。笔者实现的简单版本如下:</p><p>核心思路是维护同一个地址的 load 和 store ,同一个地址后面的 load 一定是上一次 store 的结果。只有当出现了地址可能重叠的 store ,我们才认为这个位置的 load 是不安全。当然,由于 Mx<em> 里面不存在 reinterpret_cast, void </em> 等危险指针操作,所有类型都是可以追溯的,因此我们可以根据 load/store 的类型来判断是否安全。同时,如果 load/store 的是一个类的成员,那么同一个类的不同成员的地址也是不会重叠的。如果不能保证不会重叠,那么我们只能做最坏假设: 认为已经重叠,该值可能失效。当然,全局变量之间肯定不会重叠。因为没有取地址操作,这也使得整个优化可以变得更加激进一些,不用考虑各种阴间 corner cases.</p><h2 id="Inlining"><a href="#Inlining" class="headerlink" title="Inlining"></a>Inlining</h2><p>inline 是老熟人了,这里也就不多说啥了,简单来说就是把部分函数代码内嵌到当前位置。不过,inline 还是有不少细节的 (花了我整整6个小时的说) 。首先是要跑函数调用图。由于函数之间可能会互相调用,因此我们需要先用 tarjan 算法缩点,把那些环形的调用关系缩成一个点。在此之后,函数调用图 call-graph 满足 DAG 。我们可以在 DAG 上按照拓扑序逐一 inline 各个函数。当然,每次 inline 完,我们也需要跑一遍优化 pass,因为 inline 完往往能产生新的优化点。例如:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">add</span><span class="params">(<span class="type">int</span> x,<span class="type">int</span> y)</span> </span>{ <span class="keyword">return</span> x + y; }</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="type">int</span> x = <span class="number">100</span>;</span><br><span class="line"> <span class="type">int</span> y = <span class="number">10</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">add</span>(x,-y); <span class="comment">// inline + 常量传播直接变成 90</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="loop-related"><a href="#loop-related" class="headerlink" title="loop-related"></a>loop-related</h2><p>由于 Dark 有空了,这一部分又出来了。</p><p>在寒假中,笔者重构了自己的编译器。</p><h3 id="loop-nest-tree"><a href="#loop-nest-tree" class="headerlink" title="loop-nest-tree"></a>loop-nest-tree</h3><p>首先,需要获得循环信息。<del>一个简单可行的方法是直接在 build IR 的时候往标准块上记录一些 metadata</del> 对于一个 general 的 natural loop (换句话说,没有 goto 带来一些奇怪的控制流),其有这些显著特征:</p><ul><li>循环的入口唯一存在,称作 loop header。其支配了所有循环内的块。</li><li>循环必定存在至少一个 back-edge,即从循环体的某个块跳到 loop header。</li><li>循环之间要么嵌套,要么完全不相交。</li></ul><p>因此,我们可以简单的寻找所有的 back-edge (即从一个块 A ,跳到一个支配块 A 的块 B) ,记录所有的 B ,即为所有的 loop header,同时把 A 记录为 loop body 的一部分。然后,对于所有的 loop header ,我们从目前 loop body 开始,沿着 <strong>反向流图</strong> bfs/dfs ,直到遇到 loop header ,这样我们就找到了这个循环所有的 loop body 块。</p><p>循环之间可能存在嵌套关系,我们可以用一个树形结构来表征这种关系。注意到,两个循环只可能嵌套或完全不相交,不存在其他的情况。因此,我们之前得到的 loop body 两两之间,要么一个完全被另一个包含,要么完全不相交。通过简单的枚举 (但实际笔者的实践借助了支配关系稍稍优化) 即可构建出嵌套关系的树,即为 loop-nest-tree 。</p><h3 id="loop-invariant-code-motion"><a href="#loop-invariant-code-motion" class="headerlink" title="loop-invariant-code-motion"></a>loop-invariant-code-motion</h3><p>事实上,笔者并没有写这个,而是实现了一个更加 general 的 pass: global code motion。具体细节可参考那篇经典的 GCM/GVN 的论文。</p><p>回想一下所谓的 LICM ,其所做的不过是把一些语句移动到了其他的地方。而之所以可以这么做,是因为在 SSA 形式上,只要一个语句 use 在到达这个语句之前都已经被定义了,其即为合法。除了内存操作/函数调用/输入输出 等含有副作用的语句,其他语句都是可以安全的交换顺序,只要满足了合法性。因此,我们可以考虑激进地移动部分语句。</p><p>具体而言,我们先固定那些含副作用的语句 (load/store/call/控制流相关),然后根据 use->def 关系后序去遍历所有的指令,确定每个语句可以被定义的最早的位置 (以基本块为单位)。对于任意基本块 A 中某一语句,我们只需保证该语句所有的 use,其所被定义时所在的块支配了块 A 即可。这个过程被称作 scheduleEarly,伪代码如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">scheduleEarly</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">markFixed</span>();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> block : blocks)</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> inst : block->insts)</span><br><span class="line"> <span class="built_in">dfs</span>(inst);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">markFixed</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> block : blocks)</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> inst : block->insts)</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">isFixed</span>(inst))</span><br><span class="line"> first[inst] = block;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> first[inst] = entry;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Post-order dfs.</span></span><br><span class="line"><span class="comment"> * First work out all uses of an inst.</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">dfs</span><span class="params">(instruction *inst)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">isFixed</span>(inst)) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">if</span> (visited[inst]) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> visited[inst] = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">auto</span> block = entry;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> use : inst->uses) { </span><br><span class="line"> <span class="built_in">dfs</span>(use);</span><br><span class="line"> <span class="comment">// Choose the deepest one in the dominator tree.</span></span><br><span class="line"> <span class="keyword">if</span> (first[use]->depth > block->depth)</span><br><span class="line"> block = first[use];</span><br><span class="line"> }</span><br><span class="line"> first[inst] = block;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同理,我们也可以对应的 scheduleLate,即为确定每个语句可以被定义的最晚的位置。我们只需根据 def->use 关系,保证 def 被用到的地方都被 def 所在的块所支配即可。换言之,def 所在块是其所有 use 可以处于的最晚的块的 LCA (最近公共祖先) 即可,代码几乎完全一致,这里就不再赘述。</p><p>在上面 scheduleLate 的 dfs 函数返回之前,我们得到了每条指令可以位于的最早的块 first 和最晚的块 last (事实上,其是支配树上连续的一段树链,沿着 last 往上跳可以跳到 first)。因此,我们可以尝试规划每条指令所处于的位置。首先,我们显然会让其 loop-nest 的深度尽可能地浅。我们挑选 first 到 last 中最浅地一个块即可。这样的块可能有很多,我们选择就近原则,即尽可能晚地放置这条指令。最后,选择这个循环嵌套深度最浅,且在这个深度上最晚的块,作为真正的 last 返回并记录。</p><p>具体细节建议参考论文,当然论文里面貌似伪代码是错的,建议结合支配关系好好想明白后再动手。</p><h3 id="loop-induction-variable"><a href="#loop-induction-variable" class="headerlink" title="loop-induction-variable"></a>loop-induction-variable</h3><p>很多循环都存在归纳变量,即在一次循环过程中,仅仅加或乘一个常数。而对于常量加上循环变量,其很容易带来一些强度下降,并且给我们带来更多的循环信息(比如循环的次数等等)。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">func1</span><span class="params">(<span class="type">int</span> *x)</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i < <span class="number">100</span>; ++i) x[i] = i % <span class="number">32</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func2</span><span class="params">(<span class="type">int</span> *x)</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>, *y = x; i < <span class="number">100</span>; ++i) *(y++) = i % <span class="number">32</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上例,两个函数是完全一样的,但是后者在循环内会少一次左移操作 (用来求数组的偏移量),这便是因为 IR 中 getelementpr 指令的基地址是循环不变量,而偏移量又是归纳变量,我们知道其改变规律。因此我们可以不用借助归纳变量,直接操纵基地址,这样可以减少一次左移操作,而每次循环少一次操作,这个优化力度是很大的。</p><p>同时,由于我们知道了 i 的范围,i 一定是非负的,因此 i % 32 可以进一步的被优化为 i & 31 ,这样可以减少一次取模操作,这个优化力度也是很大的,而且非常常见、通用。</p><p>以上是归纳变量优化的基本运用。进阶优化还有很多,例如循环展开、把循环连续拷贝改为 memcpy/memmove 等等。极端的优化甚至可以直接把循环求和优化为等差数列求和公式,但是针对性较强,且比较复杂,笔者暂时没有实现。</p><h2 id="Scalar-replacement"><a href="#Scalar-replacement" class="headerlink" title="Scalar-replacement"></a>Scalar-replacement</h2><p>标量替换指的是把一个没有用到取地址操作的类直接拆成几个标量。例如把一个含有两个 int 的类拆成两个 int 局部变量。</p><p>然而,由于 Mx * 类似 Java 的语法,这导致所有类都是引用类型,而且成员函数 (包括构造函数) 都是需要用到地址(指针)的,标量替换的前提看似很难满足。但是,我们可以一步一步来。</p><p>首先,我们可以通过分析函数调用关系,我们容易得到哪些 new 出来的类 (其实就是 malloc 的空间) 是已经泄露的。泄露指的是这个分配的空间被存到了其他的地方,导致其离开当前函数作用域之后依然可能被访问到,直接泄露的形式有 ret 和 store ,其也可能通过函数调用,phi 函数传播。</p><p>对于那些没有泄露的变量,有一个非常显然的小优化: 我们可以把 malloc 优化为 alloca 。是的,因为其在函数作用域结束后不再会被访问,所以我们可以用更加紧密的栈空间代替堆空间,其可以增加缓存命中率,防止污染缓存,效果还是很好的,尤其是对于那些不是特别大的中小类型。</p><p>然后,再注意到我们其实有 inline 优化,因此很多时候我们会把短的成员函数直接内联了,从而实际上最后我们对于一个类指针的操作仅限于 getelementptr 。在这样的情况下,我们便可以把这个类标量替换,拆为其各个成员。显然,聪明的您肯定也发现了,那些可能被潜在标量替换的变量,一定也是 malloc 优化成为的 alloca 变量。</p><p>以上便是本人标量替换的主体思路。由于时间限制,笔者只完成了标量替换的第一步: 把未泄露的小类型的 malloc 语句替换为了 alloca 。在特定的测试函数中,其性能能提升 10 倍甚至是 9 倍,当然核心原因还是因为其对缓存友好,且避免了费时的 malloc。</p><h2 id="Others"><a href="#Others" class="headerlink" title="Others"></a>Others</h2><p>其他小优化还有一大堆,但是时间限制暂时不详细写了。包括但不限于:</p><ul><li>尾递归优化</li><li>Mx *语言特性优化</li><li>buitlin 函数内联</li><li>UB 信息利用</li></ul><h1 id="ASM"><a href="#ASM" class="headerlink" title="ASM"></a>ASM</h1><p>暂时先摸了。计划讲讲线性染色 Linear scan register allocation 。事实上,笔者写的超级烂,现在还在改,之前写的 Linear scan 可以说是完美结合了 Linear scan 质量低的缺点和 Graph coloring 非线性的缺点。</p><p>计划改为 SSA 上寄存器分配,其不仅线性而且质量不低。不过前提是我的有空啊啊啊</p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p><del>没人会看到这里的吧</del>我也不知道为什么,但是这次编译器写的我很累,可能是因为自己真的去证明了很多优化的正确性以及复杂度,同时还自己构思了很多优化(虽然基本都是论文里面的弱化版),或许以后不应该浪费这么多时间自己瞎想。写的真的挺烂的,烂的不能再烂了,收集了一堆信息却几乎没用上多少,最后榜一还是偷上来的……唯一的好处,或许是对于 C++ 工程代码的经验积攒了不少。</p>]]></content>
<summary type="html">一个简单语言的编译器的实现</summary>
<category term="计算机" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/"/>
<category term="编译器" scheme="http://darksharpness.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA/%E7%BC%96%E8%AF%91%E5%99%A8/"/>
<category term="编译器" scheme="http://darksharpness.github.io/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"/>
<category term="优化" scheme="http://darksharpness.github.io/tags/%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>第十一届上海 THO · 东方露明境 游记</title>
<link href="http://darksharpness.github.io/SHTHO11/"/>
<id>http://darksharpness.github.io/SHTHO11/</id>
<published>2023-08-23T17:01:49.000Z</published>
<updated>2023-08-23T18:01:49.000Z</updated>
<content type="html"><![CDATA[<p>警告,本文含有大量图片,可能会比较卡顿。国内用户可尝试挂加速器加速。图总大小约为 85.5 MB,使用流量的用户请慎重点开 !</p><p>第一次参加 THO ,心里还是非常激动的。感谢 <a href="https://github.com/zsq259">Hastin</a> 同学的陪伴,玩的很开心。</p><p>先开个文章占个位吧。会更新的~</p><p>相信我~</p><p>由于某些奇怪的原因,我摆了。但是图片还是会放出来的。</p><h1 id="无分类"><a href="#无分类" class="headerlink" title="无分类"></a>无分类</h1><p><img src="https://s2.loli.net/2023/10/02/N7dyJExAUoa2bGP.jpg" alt="会场外围"></p><p><img src="https://s2.loli.net/2023/10/02/oKTviEgHD28OeUR.jpg" alt="入口"></p><p><img src="https://s2.loli.net/2023/10/02/l8NqJAUEexogsvk.jpg" alt="mc 幻想乡"></p><p><img src="https://s2.loli.net/2023/10/02/qHlhnyiuMemR3JL.jpg" alt="惊现群友"></p><p><img src="https://s2.loli.net/2023/10/02/y4urRaAiqUsvCNh.jpg" alt="琪露诺可爱捏"></p><p><img src="https://s2.loli.net/2023/10/02/yzoB7YRUvDqg8Cb.jpg" alt="东方红红蓝(指星莲船最劣开碟)"></p><p><img src="https://s2.loli.net/2023/10/02/a2AyJdX4DNTilfc.jpg" alt=""></p><p><img src="https://s2.loli.net/2023/10/02/CZXGHIejyqtmcu2.jpg" alt="一些帅气的立牌"></p><p><img src="https://s2.loli.net/2023/10/02/Tc14h9nsSOjEH6e.jpg" alt="fumo!FUMO!!"></p><p><img src="https://s2.loli.net/2023/10/02/VCYPtr9v2QB5zsm.jpg" alt="森萝的队伍真的离谱"></p><h1 id="摊位照片"><a href="#摊位照片" class="headerlink" title="摊位照片"></a>摊位照片</h1><p>以下是本人去逛过并且买过东西或者留下了很深的印象的一些摊位: (还有很多忘记拍照片了())</p><p><img src="https://s2.loli.net/2023/10/02/Q2ZHkCgTjU39v76.jpg" alt="森罗万象 (不过不能拍歌姬和编曲(悲),但 aya 可爱捏)"></p><p><img src="https://s2.loli.net/2023/10/02/OeCF6EYh2IKZxUJ.jpg" alt="Liz Triangle (算是前一个的姐妹社团了,有点意思)"></p><p><img src="https://s2.loli.net/2023/10/02/tMY4vG2cTSJzVeP.jpg" alt="SHI 之境界"></p><p><img src="https://s2.loli.net/2023/10/02/ocv9W4bDmAHUsG6.jpg" alt="上海大学 东方文化研讨(你交怎么没这玩意)"></p><p><img src="https://s2.loli.net/2023/10/02/6mHvuKFLTiIrgPU.jpg" alt="Minecraft 幻想乡(不过此类主题的服务器应该不少)"></p><p><img src="https://s2.loli.net/2023/10/02/qACzFbLoxV9TDYU.jpg" alt="Static world(封面很好看!)"></p><p><img src="https://s2.loli.net/2023/10/02/BpJ4gqSZPGXlD9T.jpg" alt="二重不眠症 (老熟人了, CP29 刚见过面 , 可惜太累了去不了 live)"></p><p><img src="https://s2.loli.net/2023/10/02/IgGZywkFiplb8n6.jpg" alt="少女分形 & 幽闭星光 (曾经的信仰社团,可惜歌姬和编曲没来)"></p><p><img src="https://s2.loli.net/2023/10/02/1USjFagPfH8osEe.jpg" alt="FUMO only (但是不卖 fumo 差评(bushi))"></p><p><img src="https://s2.loli.net/2023/10/02/FspOLqDmctXl8jZ.jpg" alt="妖精的冰屋 (没错就是前面可爱的琪露诺给吸引来的)"></p><p><img src="https://s2.loli.net/2023/10/02/vouXFaGyQ2bpLMH.jpg" alt="绯春研究会 (原来你也喜欢莉莉白小可爱~)"></p>]]></content>
<summary type="html">新人,第一次参加 THO~</summary>
<category term="随笔" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>C++ 20 部分特性尝鲜</title>
<link href="http://darksharpness.github.io/cpp20/"/>
<id>http://darksharpness.github.io/cpp20/</id>
<published>2023-07-06T08:00:37.000Z</published>
<updated>2023-07-28T08:00:37.000Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>2023 年了,C++ 23 都要出了,各大编译器厂家对 C++ 20 的支持终于有点进展了。在 GCC 最近(截止 2023-07-06) 的一次版本更新中,终于添加了对于 std::format 的支持。作为一个坚定的 GCC 追随者,笔者自然是选择 g++ 13.1 作为自己的编译器。(笑)</p><h2 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h2><p>CPU: 12th Gen Intel(R) Core(TM) i7-12700H</p><p>操作系统: Microsoft Windows 版本22H2<br>(OS内部版本22621.1848)</p><p>GCC version 13.1.0 (x86_64-posix-seh-rev1, Built by MinGW-Builds project)</p><h2 id="其他的话"><a href="#其他的话" class="headerlink" title="其他的话"></a>其他的话</h2><p>笔者希望读者在正式阅读之前,能够记住以下几个要点:</p><p><img src="https://s2.loli.net/2023/07/07/pljTPKDe12JAmxt.png" alt="Codemate? Codegpt!"></p><p>当然,不排除以上是 Codemate 瞎扯的,但是笔者自己的确能很强烈地体会到,C++ 这门语言讲究的就是高效性和包装性。它既要求能拥有和 C 一样的性能,接近底层,也希望能在此基础上提供尽可能多的包装,从而降低方便程序员更好高效的写代码。</p><p>简而言之,性能优先,在此基础上提供尽可能多的便利。这大概就是笔者经历了这一年的 coding 后对于 C++ 的理解罢。欢迎各位讨论~</p><h1 id="concept-amp-requires"><a href="#concept-amp-requires" class="headerlink" title="concept&requires"></a>concept&requires</h1><p>说是话,这可能是我最期待的一个功能了,要想真的想要深入了解,请参考 <a href="https://en.cppreference.com/w/cpp/language/constraints">cpprefence</a></p><h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><p>可能的前置知识: <a href="https://en.cppreference.com/w/cpp/language/sfinae">SFINAE</a></p><p>在 concept 出现之前,设想一下,你需要设计一个 sort 函数。一般情况下,你期望用户传入的是一个连续的序列,即数组之类的。很不幸的是,你的用户不一定有这样的觉悟。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><bits/stdc++.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> std::list <<span class="type">int</span>> l {<span class="number">1</span>,<span class="number">1</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">1</span>,<span class="number">4</span>};</span><br><span class="line"> std::<span class="built_in">sort</span>(l.<span class="built_in">begin</span>(),l.<span class="built_in">end</span>());</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于标准库的 sort 函数,当用户试图传入不支持随机访问的迭代器,就会出现类似下面的报错信息。</p><p><img src="https://s2.loli.net/2023/07/07/eGMABEJKSi7ahZT.png" alt="超过 100 行了哦~~~"></p><p>这看起来就令人十分的恼火,<del>太长不看!</del>。而更加令人恼火的是,如果代码补全是 VScode 默认的 C/C++ intellisense ,这破玩意甚至不会提示有错! 原因很简单,因为用户传入的参数匹配上了这个函数模板,而这个模板出错是在编译的时候才发现的,因此你的代码补全机器很多时候不会察觉到这个问题。这时候,聪明的你可能想要对其做出一些针对性的优化。于是,你想到了 SFINAE ,这个 C++ 14 就出现的特性。你把 sort 函数进行了巧妙的包装:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><bits/stdc++.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>,std::<span class="type">enable_if_t</span> <std::__is_random_access_iter <T>::value,<span class="type">int</span> *> = <span class="literal">nullptr</span>></span><br><span class="line"><span class="type">void</span> <span class="built_in">my_sort</span>(T __beg,T __end) {</span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">sort</span>(__beg,__end);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">signed</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> std::list <<span class="type">int</span>> l {<span class="number">1</span>,<span class="number">1</span>,<span class="number">4</span>,<span class="number">5</span>,<span class="number">1</span>,<span class="number">4</span>};</span><br><span class="line"> std::vector <<span class="type">int</span>> a {<span class="number">1</span>,<span class="number">9</span>,<span class="number">1</span>,<span class="number">9</span>,<span class="number">8</span>,<span class="number">1</span>,<span class="number">0</span>};</span><br><span class="line"></span><br><span class="line"> <span class="built_in">my_sort</span>(l.<span class="built_in">begin</span>(),l.<span class="built_in">end</span>());</span><br><span class="line"> <span class="built_in">my_sort</span>(a.<span class="built_in">begin</span>(),a.<span class="built_in">end</span>());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这下好了,对于传入非随机访问迭代器的参数,其在匹配函数的时候就完全匹配不上这个函数模板,因此在编译之前,你的代码补全工具应该就会告诉你: 没有与参数列表匹配的 函数模板 “my_sort” 实例。而编译后的报错信息也会短不少。不仅如此,有了 SFINAE,你可以为其他不满足的类型提供特定的重载。</p><p><img src="https://s2.loli.net/2023/07/07/zPJtmoSVXwpZbi9.png" alt="短了不少"></p><p>但是这样的问题也是很明显的。对于每个需要验证传入参数时随机访问迭代器的类,你都要写一个又臭又长的 std::enable_if_t…… 这真的太蠢了!完全不符合代码复用和简洁性!</p><p>不仅如此,SFINAE 还存在自己的问题: SFINAE 必须占据函数签名/函数返回值的一部分参数,例如模板参数,函数参数,函数返回值等等。而 SFINAE 占据的参数不能是在函数内部的,本质上还是因为它是在模板进行替换(substitution)操作的时候才检查模板是否可以的。如下是三种常见的 SFINAE 实现方式。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>,std::<span class="type">enable_if_t</span> <std::__is_random_access_iter <T>::value,<span class="type">int</span> *> = <span class="literal">nullptr</span>></span><br><span class="line"><span class="type">void</span> <span class="built_in">my_sort1</span>(T __beg,T __end) {</span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">sort</span>(__beg,__end);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">my_sort2</span><span class="params">(T __beg,T __end,std::<span class="type">enable_if_t</span> <std::__is_random_access_iter <T>::value,<span class="type">int</span> *> = <span class="literal">nullptr</span>)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">sort</span>(__beg,__end);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line">std::<span class="type">enable_if_t</span> <std::__is_random_access_iter <T>::value,<span class="type">void</span>></span><br><span class="line"><span class="built_in">my_sort3</span>(T __beg,T __end) {</span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">sort</span>(__beg,__end);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更加 modern 的写法</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">my_sort3_1</span><span class="params">(T __beg,T __end)</span> ->std::<span class="type">enable_if_t</span> <std::__is_random_access_iter <T>::value,<span class="type">void</span>> </span>{</span><br><span class="line"> <span class="keyword">return</span> std::<span class="built_in">sort</span>(__beg,__end);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>当然,你可以用 void_t 等方式来解决此类问题。但这样的模式还是太麻烦了点,特别是当这个东西耦合入代码,就会使得代码的可读性大大下降,直观性也不足,尽管解决了编译报错信息过长的问题。</p><p>最后呢,SFINAE 必须结合模板食用,而很多函数是不能模板化的(例如构造函数等等)。</p><p>这时候,concept 和 requires 作为一个替代方案,它出现了。其把函数的约束条件抽象出来,从而极大地增加了代码的可读性直观性,也让重复的工作得以极大的减少。</p><h2 id="Definition"><a href="#Definition" class="headerlink" title="Definition"></a>Definition</h2><p>concept 最基本的形式如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <...></span><br><span class="line"><span class="keyword">concept</span> ... = constraints......</span><br></pre></td></tr></table></figure><p>其中,等号右边 concept 是一个可以在编译期就能被估值为 bool 类的一系列函数或表达式,其非常像 constexpr bool 变量。举例:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 判断是不是原生浮点类型,即是不是 float/double</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> is_floating_type = std::is_same_v <T,<span class="type">float</span>> || std::is_same_v <T,<span class="type">double</span>>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 输出 true false */</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">()</span> </span>{</span><br><span class="line"> std::cout << std::boolalpha;</span><br><span class="line"> std::cout << is_floating_type <<span class="type">float</span>> << <span class="string">'\n'</span>;</span><br><span class="line"> std::cout << is_floating_type <<span class="type">long</span>> << <span class="string">'\n'</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">bool_true</span> {</span><br><span class="line"> <span class="type">int</span> x;</span><br><span class="line"> <span class="comment">// 注意,这个 constexpr 是必须的!</span></span><br><span class="line"> <span class="function"><span class="keyword">constexpr</span> <span class="keyword">operator</span> <span class="title">bool</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">return</span> x; }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> always_true = <span class="built_in">bool</span>(bool_true{<span class="number">3</span>});</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而使用一个 concept 有很多种方法。虽然这有点像茴香豆的四种写法,但我觉得还是有必要都了解一下的,当然平时只要会用就行了。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> signed_integer = std::is_integral_v <T> && std::is_signed_v <T>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <signed_integer T></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">lowbit1</span><span class="params">(T x)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> x & (-x);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="comment">// 这里可以 requires 很多个 constexpr bool/concept 哦</span></span><br><span class="line"><span class="keyword">requires</span> signed_integer <T> && <span class="function"><span class="literal">true</span> </span></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">lowbit2</span><span class="params">(T x)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> x & (-x);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">lowbit3</span><span class="params">(T x)</span> <span class="keyword">requires</span> signed_integer <T> </span>{</span><br><span class="line"> <span class="keyword">return</span> x & (-x);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">lowbit4</span><span class="params">(signed_integer <span class="keyword">auto</span> x)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> x & (-x);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 如果你把下面这些整数变为 10f(float 类)/ 10u(unsigned),</span></span><br><span class="line"><span class="comment"> * 你的 intellisense 应该会报错,编译也不会通过。</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">()</span> </span>{</span><br><span class="line"> std::cout </span><br><span class="line"> << <span class="built_in">lowbit1</span>(<span class="number">10</span>) << <span class="string">' '</span></span><br><span class="line"> << <span class="built_in">lowbit2</span>(<span class="number">12</span>) << <span class="string">' '</span></span><br><span class="line"> << <span class="built_in">lowbit3</span>(<span class="number">14</span>) << <span class="string">' '</span></span><br><span class="line"> << <span class="built_in">lowbit4</span>(<span class="number">16</span>) << <span class="string">'\n'</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而 requires ,顾名思义就是 要求 (require) 对于函数模板产生一定的约束(误)。确实是这么一回事,但 requires 不只是这些,其不仅可以后接 constexpr 变量和定义好的 concept (正如前面 lowbit2 函数演示的那样),作为标识符检查值是否为 true,其还可以添加任意的表达式,检验内部表达式是否存在,自身充当一个 constexpr bool 变量。下为伪代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">requires</span> <span class="comment">//含 concept 和 constexpr bool 逻辑表达式,相当于一个完整的语句</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 以下两种 requires 则更像是一个表达式</span></span><br><span class="line"><span class="keyword">requires</span> { <span class="comment">/* 具体约束,只会判断能否过编译,不会真实执行。 */</span> }</span><br><span class="line"><span class="built_in">requires</span> (<span class="comment">/* 参数表,类似函数参数 */</span>) { <span class="comment">/* 具体约束 */</span> }</span><br></pre></td></tr></table></figure><p>当然,这么说还是太抽象了点,下面举一些简单的例子:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 注意,concept 也可以有默认参数,虽然一般没啥用</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span> = <span class="type">void</span>></span><br><span class="line"><span class="keyword">concept</span> A = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// requires 最基础的用法,检测后面表达式是否为 constexpr true</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="built_in">requires</span> (<span class="literal">true</span> && A <T>) || <span class="function"><span class="literal">false</span></span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func1</span><span class="params">(T)</span> </span>{</span><br><span class="line"> <span class="keyword">throw</span>;</span><br><span class="line"> <span class="built_in">static_assert</span>(<span class="literal">false</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 注意区分下面两个 requires 的区别</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">requires</span> <span class="keyword">requires</span> {</span><br><span class="line"> <span class="built_in">func1</span>(<span class="number">114514</span>);</span><br><span class="line">} && (<span class="built_in">sizeof</span>(T) == <span class="number">4</span>)</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">tester</span> {};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> arithable = A <> && A <T> && <span class="built_in">requires</span>(T x,T y) {</span><br><span class="line"> x + y;</span><br><span class="line"> x - y;</span><br><span class="line"> x * y;</span><br><span class="line"> x / y;</span><br><span class="line"> <span class="keyword">typename</span> tester <T>; <span class="comment">// 检测类型是否存在</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> is_container = <span class="built_in">requires</span> (T x) {</span><br><span class="line"> x.<span class="built_in">size</span>(); <span class="comment">// 检测成员函数/成员变量</span></span><br><span class="line"> x.<span class="built_in">empty</span>();</span><br><span class="line"> x.<span class="built_in">clear</span>();</span><br><span class="line"> <span class="keyword">typename</span> T::iterator; <span class="comment">// 检测类型是否存在</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 注意区分下面的两个 requires</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> is_custom = <span class="built_in">requires</span> (T x) {</span><br><span class="line"> <span class="comment">// 这里的 requires 是 requires 语句</span></span><br><span class="line"> <span class="keyword">requires</span> is_container <<span class="keyword">decltype</span>(x.x)>;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">custom_t</span> { std::vector <<span class="type">int</span>> x; };</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>简而言之,存在两类 requires: 第一种 requires 是 requires 语句, 它检测其后的表达式是否为 constexpr bool true ,如果不满足则直接<del>原地爆炸</del>认为是匹配失败,可以继续尝试匹配其他的模板。第二种 requires 则是 requires 表达式,带有约束列表(以及可能带有参数),其本身会在编译时被替换为 constexpr bool 变量。</p><p>什么,你不信? 看看输出的是啥类型。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span> = <span class="type">void</span>></span><br><span class="line"><span class="keyword">concept</span> nothing = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">print_type</span><span class="params">(T)</span> </span>{</span><br><span class="line"> std::cout << <span class="string">"others\n"</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <></span><br><span class="line"><span class="type">void</span> print_type <<span class="type">bool</span>> (<span class="type">bool</span>) {</span><br><span class="line"> std::cout << <span class="string">"bool\n"</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">print</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">constexpr</span> <span class="keyword">auto</span> __r = <span class="keyword">requires</span> { <span class="built_in">sizeof</span>(<span class="type">int</span>); };</span><br><span class="line"> <span class="built_in">print_type</span>(__r);</span><br><span class="line"> <span class="built_in">print_type</span>(nothing <>);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>需要注意的是,concept 不能递归其本身,而 concept 也不能被约束。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">concept</span> A = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 不可以递归!</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>,<span class="keyword">class</span> <span class="title class_">V</span>></span><br><span class="line"><span class="keyword">concept</span> Recursive = std::is_same_v <T,V> ||</span><br><span class="line"><span class="built_in">requires</span> (T x) {</span><br><span class="line"> <span class="keyword">requires</span> Recursive <<span class="keyword">decltype</span>(x.x),V>;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <A T></span><br><span class="line"><span class="keyword">concept</span> Error1 = <span class="literal">true</span>;</span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>> <span class="keyword">requires</span> A <T></span><br><span class="line"><span class="keyword">concept</span> Error2 = <span class="literal">true</span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看出,concept 和 requires 表达式本质上就是一个 constexpr bool 模板变量,而 requires 语句则是用来辅助 SFINAE 来约束模板类型。在用了 concept 和 requires 之后,代码逻辑变得更加清晰了,SFINAE 不会再和模板逻辑耦合了,再也不用在模板里面放一个又臭又长的 SFINAE 专用参数了(笑)。</p><h1 id="Threeway-Comparision"><a href="#Threeway-Comparision" class="headerlink" title="Threeway_Comparision"></a>Threeway_Comparision</h1><p>在看这个之前,简单回顾下笔者所认为的 C++ 的核心: 性能优先,包装性其次。OK,那我们来考虑一下以下的情景作为引入:</p><h2 id="Intro"><a href="#Intro" class="headerlink" title="Intro"></a>Intro</h2><p>现在,假设你实现了一个简单的、支持动态扩容的字符串类,如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> dark {</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">string</span> {</span><br><span class="line"> <span class="comment">/* 具体实现...... */</span></span><br><span class="line">};</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>作为一个字符串类,除了常见的如 size(),下标访问等接口,自然也是需要支持比较的,而比较的运算符有 >,<,≤,≥,≠,== 这些,于是 string 类型直接的比较就需要重载 6 个函数。当然,字符串比较非常简单,你可以直接调用 strcmp 函数进行比较。看到这里,比较仔细且特别关注性能的读者这里可能已经察觉到了一丝异常,你先别急,后面会分析的。</p><p>然而,最麻烦的还不是这个。作为字符串类型,自然要为原生的字符数组类型 char <em> 提供支持,具体体现在可以由原生的 char </em> 数组构造 string,且 string 必须支持和 char <em>进行比较等操作。然而,重载 string 和 char </em> 的比较有两种情况,string (比较运算符) char <em>,以及 char </em> (比较运算符) string 。在 C++ 中,你必须为这两种情况都分别提供特殊的重载,否则,string 和 char <em> 在比较的时候,char </em> 可能会调用隐式构造函数,转化为 string 类型,而这多了一层不必要的开销!</p><p>这样的话,不出意外,你需要写 6 <em> 3 个比较函数。这也太重复了吧! 事实上,对于任意两个字符串的比较,其总是可以归结为两个 const char </em> 进行比较,因此我们可以借助 strcmp 函数来实现,代码类似如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Compare_Less</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *x,<span class="type">const</span> <span class="type">char</span> *y)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">strcmp</span>(x,y) < <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>事实上,这背后还有不少的小麻烦。当你将两个字符串绑在一起,作为一个 pair 的时候,你的比较函数的重载将会非常麻烦。如果按照传统的写法,那可能是这样。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> pair = std::pair <std::string,std::string>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Compare_Less</span><span class="params">(<span class="type">const</span> pair &lhs,<span class="type">const</span> pair &rhs)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(lhs.first < rhs.first) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span>(rhs.first < lhs.first) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> lhs.second < rhs.second;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此类写法有一个巨大的问题,那就是前两次比较其实可以一次完成: 如果某个比较操作可以直接明确地返回两个数的相对大小关系,并且开销和一次比较差不多,那么我们用这个比较的结果就可以直接代替前两次比较,将开销较大的两次 string 类转化为一次 string 类比较加上开销较小的若干次 int 值比较,如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> pair = std::pair <<span class="type">char</span> *,<span class="type">char</span> *>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Compare_Less</span><span class="params">(<span class="type">const</span> pair &lhs,<span class="type">const</span> pair &rhs)</span> </span>{</span><br><span class="line"> <span class="type">int</span> cmp = <span class="built_in">strcmp</span>(lhs.first,rhs.first);</span><br><span class="line"> <span class="keyword">if</span>(cmp != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> cmp < <span class="number">0</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">strcmp</span>(lhs.second,rhs.second) < <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然,这种写法对于数据的要求略高,不是所有类型都像 string 那样,有一个统一的比较函数接口。</p><p>究其根本,其实是语言上缺少一种一个通用的接口,可以直接返回两个类的明确的相对大小 (即一次性不花费额外开销,得到究竟是小于,还是等于,还是大于的大小关系) 。小插曲,DarkSharpness 在写 map 的时候就意识到了这个问题,只可惜大部分的 map 要求的是 < 重载。为此,Dark 在后续 B+ 树作业中就要求传入的类自带 Compare 函数,可以返回确定的大小关系,当然这么写很不自然也很丑陋……</p><p>总之,过去的 C++ 比较存在如下问题: 需要重载很多的运算符,且不存在统一的可以一次比较出两个数相对大小的方法。</p><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>在 C++ 20 里面,终于推出了<del>宇宙飞船</del>三路比较运算符,作为比较相对大小的统一接口吗,可以说是一个最为优雅的解决方案。</p><p>当然,标准库在此基础上还加强了这一比较运算符。其返回的不是 int 参数,而是返回特殊的比较类别: std::strong_ordering / std::weak_ordering / std::partial_ordering ,对应的是强序关系,弱序关系 和 偏序关系。</p><p><strong>强序关系</strong>表示任何两个该类的对象都可以比较得出相对大小,且等价(equivalent)的两个对象完全相等(equal),即<strong>这两个对象的值是不可辨别的</strong>。整数类型便是典型的强序关系。</p><p><strong>弱序关系</strong>表示任何两个该类的对象都可以比较得出相对大小,但等价的两个对象<strong>不一定是不可辨认的</strong>。例如,将整数按照二进制位中 1 的个数排序,其满足的就是弱序关系。</p><p><strong>偏序关系</strong>表示两个该类的对象不一定可以比较得出相对大小。例如浮点类型,因为 NaN(not a number) 不能和其他任何东西比较。</p><p>需要注意的是,<strong>等价</strong>(equivalent)和<strong>相等</strong>(equal)往往会被混淆,这可能是因为各位平时都习惯了强序结构……偶尔看看英文还是有好处的,有助于区分某些概念(好吧当年学英语的时候,这两个词也搞了 Dark 一段时间)。</p><p>在使用三目运算符的时候,可以简单的将三目运算符的返回结果和 0 进行比较。如果 < 0 ,那就是小于关系。如果 = 0 ,那就是等价(或相等)关系。否则,就是大于关系。其用法类似 strcmp 函数,这里就不多阐释了。代码如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">custom</span> {</span><br><span class="line"> <span class="type">int</span> x,y;</span><br><span class="line"> std::strong_ordering <span class="keyword">operator</span> <=> (<span class="type">const</span> custom &rhs) <span class="type">const</span> {</span><br><span class="line"> <span class="keyword">return</span> x == rhs.x ? y <=> rhs.y : x <=> rhs.x;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="Bonus"><a href="#Bonus" class="headerlink" title="Bonus"></a>Bonus</h2><p>如果只有三目运算符,并不能称得上什么大革新,只能说为了性能做出的一个统一接口罢了。C++ 20 在三目运算符的基础上,还推出了自动生成运算符的方法。</p><p>是的,只要类里面实现了三目运算符,C++ 就会自动为我们生成包括 >,<,≤,≥ 这四个常见的 operator 。同时,C++ 也提供了默认的三目运算符的方法,其讲按照变量定义的顺序,数组的下标从小到大,逐一去比较两个类的成员,类似字典序。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">conless</span> {</span><br><span class="line"> <span class="type">int</span> x;</span><br><span class="line"> std::string y;</span><br><span class="line"> <span class="type">double</span> z[<span class="number">2</span>];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> <span class="keyword">operator</span> <=> (<span class="type">const</span> conless &,<span class="type">const</span> conless &) = <span class="keyword">default</span>;</span><br><span class="line"></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>需要注意的是,当你使用 default 三目运算符的时候,最好将返回参数设置为 auto ,编译器会自动地决定其返回类型。返回类型当然取得是比较类型中最弱的那个,比如上面例子中 conless 类中,最弱的是 double 数组的 double 变量,是 std::partial_ordering ,因此 auto 推导的类型就是 std::partial_ordering 。特别地,如果存在一个成员类型,其是不可比较的,那么 default 生成的该函数返回类型是 void,因此编译器不会自动生成其他的几个比较函数。</p><p>讲到这里,细心的你肯定已经发现了: 为什么不会帮我们自动生成 == 和 ≠ ? 这背后其实隐藏着一个性能问题:</p><p>在你比较两个 std::string 类型变量是否相等的时候,思考一下你会怎么做? 你会先比较长度! 是的。你不会统一的按照字典序的方法去比较,你当然会先比较长度,如果长度不同那么其显然不会相等。这是一个非常显著的优化。</p><p>因此为了性能,C++ 不会让默认的三目运算符生成 == 和 ≠ ,即使你依然可以通过 <code>(x <=> y) < 0</code> 来判断两个变量是否相等。当然,为了便利性,C++ 也提供了 == 的默认生成方式来生成默认的 == 和 ≠ 运算符。</p><h2 id="Implement"><a href="#Implement" class="headerlink" title="Implement"></a>Implement</h2><p>不得不说 gcc 对于三目运算符的实现还是非常有意思的。首先,要使用三目运算符,需要一个 <compare > 头文件,这个头文件里面有关于 std::strong_ordering 等序结构类的定义。</p><p>通过查看头文件,我们不难发现,其实所谓的大于小于等于的关系,都是用一个 char 变量来表示,其值可以是 -1 (less) 或 0 (equal/equivalent) 或 1 (greater) 或 2 (unordered)。</p><p>不过问题出现了: 你要支持序结构和整数 0 之间比较的操作,例如 <code>(x <=> y) == 0</code> 。但是,如果为序结构提供一个转化为整数的接口,或者为整数提供一个转化为序结构的接口,都会带来各种不安全。那么,如何不依赖编译器,只借用库文件就能实现一个好的序比较呢? 这里就用到了一个 C 语言的 trick:</p><p>字面量 0 , 其在语言中可以被隐式的转换为空指针类型,类似于 nullptr 。事实上,这一设计存在某些问题,经常被人诟病,这也是为什么我们有了 nullptr。但是,在这里,比较的实现用到了这个 trick。标准库实现了一个虚空代理类 __unspec ,其没有任何成员,唯一的构造函数是传入自己的指针:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">__unspec</span> {</span><br><span class="line"> <span class="keyword">constexpr</span> __unspec(__unspec*) <span class="keyword">noexcept</span> { }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>而众所周知,0 可以隐式的转换为空指针类型……因此,事实上,你在比较的不是 0 , 而是一个 __unspec 对象,通过这个对象和序对象的比较,我们间接的得到了内部的大小关系信息。</p><p>当然,你可能觉得这样太过奇怪。没有关系,标准库也提供了其他的接口来提取出大小关系,如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> cmp = <span class="number">1</span> <=> <span class="number">2</span>;</span><br><span class="line"></span><br><span class="line">std::<span class="built_in">is_eq</span>(cmp); <span class="comment">// == </span></span><br><span class="line">std::<span class="built_in">is_neq</span>(cmp); <span class="comment">// !=</span></span><br><span class="line">std::<span class="built_in">is_lt</span>(cmp); <span class="comment">// <</span></span><br><span class="line">std::<span class="built_in">is_gt</span>(cmp); <span class="comment">// ></span></span><br><span class="line">std::<span class="built_in">is_lteq</span>(cmp); <span class="comment">// <=</span></span><br><span class="line">std::<span class="built_in">is_gteq</span>(cmp); <span class="comment">// >=</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="format"><a href="#format" class="headerlink" title="format"></a>format</h1><p>感谢 g++ 13.1 提供的对于 format 的支持,终于可以用了!</p><h2 id="Past"><a href="#Past" class="headerlink" title="Past"></a>Past</h2><p>在过去,标准的输入输出一般有两种途径,一种是 C 语言风格的 scanf/printf ,另一种是 C++ 风格的 std::cin 和 std::cout 。前者可以根据格式串输入输出,后者则是通过类的重载进行输入输出。</p><p>但是,两者都存在一定的问题。相信大家在作为初学者的时候,肯定有遇到 scanf/printf 格式串写错,参数写错等一系列的问题吧。比如:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">long</span> <span class="type">long</span> x;</span><br><span class="line"><span class="built_in">scanf</span>(<span class="string">"%lld"</span>,x); <span class="comment">// 应该是 &x</span></span><br><span class="line"><span class="built_in">printf</span>(<span class="string">"%d"</span>,x); <span class="comment">// 应该是 "%lld"</span></span><br></pre></td></tr></table></figure><p>这样的错误并不会有任何报错,甚至可能不会在运行时有任何的异常出现,直到遇到一些极端情况,出现莫名其妙的错误……</p><p>此类错误 debug 起来并不是非常容易,特别是当程序非常庞大的时候,由于编译器没有报错信息,定位到输入输出出错需要相当长的时间。</p><p>然后是 std::cin 和 std::cout 。尽管其做到了 typesafe,但其性能问题被诟病已久,即使在关闭了流同步以后,表现依然不是非常好。</p><p>然后是可读性,很难理解为了给输出表达式加括号,你需要在两边单独输出两个字符……</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">std::string str = <span class="string">"Hello World!"</span></span><br><span class="line">std::cout << <span class="string">'{'</span> << str << <span class="string">'}'</span>;</span><br></pre></td></tr></table></figure><p>因此,长久以来,C++ 社区就存在呼声,要有 scanf/printf, std::cin/std::cout 之外的标准 inout 手段,不仅要高效,而且要 typesafe。</p><p>事实上,对于 OIer,他们中的很大一部分,甚至都手写了快速读写函数,而不屑用标准库的输入输出,这就已经足以说明一部分问题了。</p><h2 id="fmtlib"><a href="#fmtlib" class="headerlink" title="fmtlib"></a>fmtlib</h2><p><a href="https://github.com/fmtlib/fmt">fmtlib</a>,一个现代的第三方 C++ 库,其提供了格式化输出字符串更加高效的写法。由于其完美兼顾了 typesafe 和高效的两方面特点,其也因此被部分的并入了 C++ 20 的标准库中。</p><p>由于功能过于繁多,这里挑取其部分特点进行介绍。</p><h3 id="Basic-Usage"><a href="#Basic-Usage" class="headerlink" title="Basic-Usage"></a>Basic-Usage</h3><p>首先,我们来讲讲他怎么用吧。非常简单,他和 printf 非常像,要提供一个格式串和参数列表。但是格式串,其远简单于 printf , 其没有必要加上 %d %f 之类的参数 ,只需要用 {} 即可。输出的则是 std::string,又有点像是 sprintf。如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// str = "1 2" </span></span><br><span class="line">std::string str = std::format(<span class="string">"{} {}"</span>,<span class="number">1</span>,<span class="string">"2"</span>);</span><br></pre></td></tr></table></figure><p>特别地,当你要输出一个字符 { 或 } 的时候,只要重复一遍即可,如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// str = "{1 2}" </span></span><br><span class="line">std::string str = std::format(<span class="string">"{{{} {}}}"</span>,<span class="number">1</span>,<span class="string">"2"</span>);</span><br></pre></td></tr></table></figure><p>当然,其功能不止如此。如果你想要进一步的操纵输出的变量,你可以在 {} 内部添加新的参数。形如: {index : rule} 。第一个 index 表示输出的变量所在的下标。当默认括弧内无参数的时候,其输出顺序就是从左往右。当括弧内存在 index 参数的时候,所有括弧必须都有 index 参数,且输出顺序按照 index 顺序。当只有 index,冒号可写可不写。特别地,这玩意甚至支持复用! 如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// str = "1919810"</span></span><br><span class="line">std::string str = std::format(<span class="string">"{1}{1:}{0}"</span>,<span class="number">810</span>,<span class="string">"19"</span>);</span><br></pre></td></tr></table></figure><p>而冒号后面的内容,则是具体格式串的要求。如下:</p><p>整数:</p><div class="table-container"><table><thead><tr><th>格式</th><th>含义</th></tr></thead><tbody><tr><td>d</td><td>十进制整数</td></tr><tr><td>x</td><td>小写十六进制整数</td></tr><tr><td>o</td><td>八进制整数</td></tr><tr><td>b</td><td>二进制整数</td></tr></tbody></table></div><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::cout << std::format(<span class="string">"{0:d} {0:x} {0:X} {0:o} {0:b}\n"</span>, <span class="number">114514</span>);</span><br></pre></td></tr></table></figure><p>浮点数:</p><div class="table-container"><table><thead><tr><th>格式</th><th>含义</th></tr></thead><tbody><tr><td>f</td><td>固定点表示法</td></tr><tr><td>e</td><td>小写科学计数法</td></tr><tr><td>E</td><td>大写科学计数法</td></tr><tr><td>g</td><td>选择最简表示法(f/e)</td></tr><tr><td>G</td><td>选择最简表示法(f/E)</td></tr></tbody></table></div><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::cout << std::format(<span class="string">"{0:f} {0:e} {0:E} {0:g} {0:G}\n"</span>, <span class="number">1919.810</span>);</span><br></pre></td></tr></table></figure><p>字符串:<br>s 表示字符串…… </p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::cout << std::format(<span class="string">"{:s} {:s}\n"</span>,std::<span class="built_in">string</span>(<span class="string">"yyu"</span>),<span class="string">"yyu"</span>);</span><br></pre></td></tr></table></figure><p>其他输出设置:</p><div class="table-container"><table><thead><tr><th>格式</th><th>含义</th></tr></thead><tbody><tr><td><</td><td>左对齐</td></tr><tr><td>></td><td>右对齐</td></tr><tr><td>^</td><td>居中对齐</td></tr><tr><td>数字</td><td>指定输出宽度</td></tr><tr><td>字符</td><td>指定填充字符(用来填充到指定宽度)</td></tr><tr><td>.数字</td><td>设置输出精度/最大字符串长度</td></tr><tr><td>+</td><td>正数输出 + 号</td></tr></tbody></table></div><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">std::cout << std::format(<span class="string">"{0:_^10s}\n{0:_<10s}\n{0:_>10s}\n"</span>,<span class="string">"yyu"</span>);</span><br><span class="line">std::cout << std::format(<span class="string">"{:.3s}\n{:.3f}\n"</span>,<span class="string">"yyuyyu"</span>,<span class="number">1.0</span>);</span><br></pre></td></tr></table></figure><p>除了 format,标准库还提供了 format_to 函数,其可以把格式化的结果输出到一个 char 数组当中。其功能类似 sprintf,但是性能更优,且支持自定义解析,而且类型安全。</p><h3 id="Compile-Time-Parse"><a href="#Compile-Time-Parse" class="headerlink" title="Compile-Time-Parse"></a>Compile-Time-Parse</h3><p>format 库最大的特点是它支持的是编译期解析,也就是其解析格式串的过程是 0 运行时开销的!</p><p>这是因为其传入的参数必须是 constexpr 的,或者说 consteval 的字符串,即必须在编译期间确定值的字符串。因此,其可以借助 constexpr 函数以及模板,</p><p>由于 Dark 实在是太<del>菜</del>懒了,所以具体的实现咕咕了。感兴趣的可以去看看 std::basic_format_string 这个类 qwq。</p><p>当然,你可能会想: 我们如何为自定义的类型使用自己的 format 格式呢? 这就需要用到自定义的 formatter 类。</p><p>由于时间问题,这里留给读者自行研究 qwq。</p><h1 id="const"><a href="#const" class="headerlink" title="const"></a>const</h1><p>这部分是关于 constexpr,consteval 和 constinit 的。由于 DarkSharpness 太咕咕了,于是这部分就没了。<del>v 我 50现在就写</del></p><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>想看啥直接发评论区,有啥讲的不好的欢迎来喷 Dark 教授,真的都可以说的,Dark 教授非常欢迎您指出错误 Orz!</p><p>当前计划列表: std::span , consteval/constinit ……</p><p>参考资料: <a href="https://www.bilibili.com/video/BV1r8411N75b/">C++20 STL Cookbook 2023</a> , <a href="https://en.cppreference.com/w/">cppreference</a></p>]]></content>
<summary type="html">C++17 都没学明白,C++20 就来了。</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/categories/C/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
<category term="C++" scheme="http://darksharpness.github.io/tags/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>2022学年下半学期总结</title>
<link href="http://darksharpness.github.io/summary2/"/>
<id>http://darksharpness.github.io/summary2/</id>
<published>2023-06-18T14:24:44.000Z</published>
<updated>2023-10-02T02:14:44.000Z</updated>
<content type="html"><![CDATA[<p>大量图片警告! 本文后半段存在大量图片,使用流量的读者慎点! 图片加载缓慢,国内用户建议使用加速器。</p><p>没想到我这个破烂还能坚持写 blog 到今天……或许 tomorrow we die alive ?</p><p>这次不写 <a href="/summary/"> 上次 </a> 那种流水账了,看着就恶心。</p><p>说实话,这学期真的没啥好总结的,开学就基本决定好要好好摆烂一学期了,结果连摆烂都没坚持下去,中间死活放不下。最后摆烂也没摆成,卷也没卷成,学习一事无成,身体每况愈下。只能说,自己选择的破路,走到底。</p><p>这次呢,我想聊聊我这学期为数不多的,特别投入的项目。当然,我只会讲我想讲的那部分。</p><h1 id="学子讲坛"><a href="#学子讲坛" class="headerlink" title="学子讲坛"></a>学子讲坛</h1><p>这学期,我学子讲坛一开始也没啥想法。但是,鉴于上学期学子讲坛彻底摆烂,分数实在是太逆天,害怕这学期再摆烂 yyu 要不高兴,所以打算认真搞一个。而且呢,我本人是很希望做一个非常有深度,而且能够内容充实,有很多论据的演讲。</p><p>凡是总是要一个动机的吗。我学子讲坛的动机呢,很简单,就是上次那片破烂流水账文章的一段。</p><p><img src="https://s2.loli.net/2023/06/18/RMQgJy9n412VWqf.png" alt="真的很简单"></p><p>而在后续与友人 <a href="https://github.com/zsq259">Hastin</a> 同学交谈的过程中,我也逐渐确立了在学子讲坛中讲好 “东方” 的想法。说实话,我也不是啥资深车万人,对于东方的了解,也很表层很不本质。但是呢,出于一种对于 “东方” 的喜爱,以及一种 “整活” 的心理,最后还是选择了这个话题。</p><p>事实上,我是打算好好讲好它的,毕竟 “东方” 在我心中还是一个非常神圣而 “美好” 的存在。我的确希望能通过这个机会,能让更多的人了解到东方,或许能挽救其下滑的趋势。当然,这样努力的效力是微弱的,即使全班人都能了解东方,那又如何。如果不能成为内容创作者,为社区出力,那么努力几乎就是白费。而我,作为所谓的 “东方爱好者” ,可以说几乎从来没为社区做出什么贡献。我不会创作作品,我也没有财力去支持自己喜欢的社团,听了那么多同人音乐,手头甚至就不到五张东方 CD……我,或许什么都没做,完全没有那种同人精神。我,最差劲了呢……</p><p>准备的过程也是非常曲折的。整个<a href="https://github.com/DarkSharpness/Touhou_Project_Forum">项目</a>全程在 github 上开源开发,进度都有记录。在开学的几周,我和 Hastin 同学其实就已经有在商讨思路。我们甚至计划能在 3 月前规划完主体,然后开学一个月内准备完 PPT。当然,这样的要求显然没有达到。我们大致确定了思路,但是由于各种原因,摆了很久。当然,我们还是都想认真讲好这个话题的,我特地每次学子讲坛都有在认真去听,甚至还会和 Hastin 一起总结,前面的人演讲哪里做的好,哪里做的不好。然后呢,我们也请到了 hjb 同学来帮我们分析,他也提供了许多非常棒的素材!</p><p><img src="https://s2.loli.net/2023/06/18/WOkUB6A9Fvu1T4K.jpg" alt="只是在吹毛求疵,请不要太较真,大家都很认真的~"></p><p>一路拖拖拖,终于拖到了四月末,距离正式开讲只有三周了。这时候,我才意识到,这样巨大的工作量不能再拖下去了。于是最后两周,我们几乎是全身心地投入其中。在这段时间里,我查了许多关于东方的资料和文章,也加入了许多严肃的东方讨论群,逐渐对于东方的过去有了一个更加全面,更加深刻的理解。作为一个文科 + 艺术属性 = 0 的人,我其实很不擅长这方面的破玩意,但是通过查阅文章,我真的接触到了很多不同人的思考,也第一次感受到这种人文方面的魅力(我也不知道该称作什么了)。</p><p>终于拖到了演讲。说实话,一上台,我就已经完全忘记了时间的存在。尽管试讲了很多次,每次都说要控制好时间,然后要控制在哪里停顿,保证效果,最后还是忘得一干二净。不过主体结构倒是没出错,细节的内容貌似也没拉下什么,不过最后强行加戏有点愚蠢,以后可不会再为了讨好 yyu 这么说了(笑)。对于最后 yyu 的点评呢,我只能说…….借题发挥(?),总之很迷惑就对了,暂时保留意见。</p><p>总之,整个学子讲坛可能是我本学期准备的最久的一个项目了,以后也肯定不会有这样的项目了,至少在学子讲坛这门课上。说实话,我也不知道为什么我要在这样一门只有 2 学分的非核心课上浪费这么多时间。或许是东方给我带来的那一份微不足道的创作欲罢了。</p><p>说了这么多,顺便谈谈我自己的一些旧事吧。在很久以前,Dark 还是很希望能创作东方同人作品的(笑),但是由于水平不够,在尝试制作同人音乐和绘画无果后放弃。说实话,很少有东西能让我这么想要表现自我,想要加入社区成为一份,也几乎没有东西能让我沉迷其中如此久。或许,这就是东方的魅力所在,这就所谓的同人精神吧,这就是幻想与浪漫吧。管他呢。</p><h1 id="Impart"><a href="#Impart" class="headerlink" title="Impart"></a>Impart</h1><p><a href="/mcImpart/"> Impart </a> ,一个 Minecraft 游戏企划。这里就不多介绍了,想必能看到这里的小朋友肯定是有所耳闻的啦。</p><p>这个计划呢,其实是我童年回忆的一部分…… Impart 最初的活动 EHC,几乎完全就是仿照我初中和高中时(主要是初中)看的那些台湾实况主的 UHC 活动而设定的。在以前,我看到了他们的视频,真的很想自己也能参加这样的活动,但是身边的条件并不允许这一点。到了大学,终于有机会能够补上童年的这块碎片了……</p><p>Minecraft,真的是最喜欢的一款游戏。具体是什么时候也忘记了,大概是小学二三年级的时候,我从同学那里接触到了 Minecraft 这款游戏。当时 MC 还是风靡于我们这里的中小学生的,就像后来的吃鸡和王者荣耀那样。但是,这款游戏不同于当时的赛尔号什么的其他游戏,他是真的抓住了我。在这款游戏中间,我切身的感受到了一种自由,那种创造一切的自由。</p><p>当然,真正变成死忠粉,还多亏了那些游戏实况主。我最早有印象认识的实况主是奇怪君。当时我刚玩手机版 MC 大概一两年,看到了他的实况视频。作为一个新手玩家,他的游戏解说很快吸引了我,指引我学会了许多 MC 的基本操作,也进而认识了 MC 的 Java 版,从此走上了 MC 的不归路。现在看来那时候的实况视频真的是无比简陋,但小时候的我,最享受的就是放学后,能够从家长那里要到手机,然后看那些 MC 视频。</p><p>小学 4 年级以后,大概是 2014 年(?),我开始高强度玩 Java 版 MC,并且活跃于各个服务器。这时候,我在优酷上,认识了舞秋风这位台湾的实况主。他的那些经典的长视频,牢牢地抓住了我,我特别喜欢那种慢节奏(当然当时的人好像都那样)的生存实况解说,那种轻松而幽默的氛围真的是令人无比愉悦。当然,现在恐怕是再也找不回来了。而没有他,我恐怕当时也不会在众多游戏中,坚持只玩 MC。</p><p>上初中后,我 MC 水平基本过关,可以勉强称得上一个 “入门玩家”,我也开始尝试玩纯生存之外的游戏,毕竟人总不能一成不变吧。这时候,多玩我的世界盒子,提供了绝佳的平台,我可以轻松在上面联机,并且和朋友一起玩耍。直到现在,我还是非常怀念这个启动器的。虽然他的确是盗版,但他也承载了我童年无数美好的回忆,无数个父母不在家的夜晚,在方块世界创造的无数奇迹。只能说,rip吧! 谢谢您给我带来的美好!</p><p>后来的我呢,在多玩没了后,开始转向 BakaXL 和 PCL 两款启动器,也对 MC 有了深刻的多的理解,不过最美好的回忆,还是最初最懵懂无知,和朋友们一起探索时候留下的。当然,如果在看的你,不是一个中度以上 Minecraft 玩家,可能也不会有这么多的感悟。</p><p>所以说,Impart 中的 EHC 活动,完全只是本人童年回忆的衍生,只是在去弥补那块缺失的拼图。(不知道为啥讲了那么多……)</p><p>然后是 Impart 的管理。说实话,我是一个非常不合格的管理员,我没能有力地号召所有玩家能同时上线,其中也有很多次活动因为人数太少而被取消,对此我还是深表愧疚的。当然,Impart 采用的是民主投票,要玩啥由大家自己决定,时间也是大家自己定,我们除了 EHC,也玩了 manhunt , bingo 等诸多项目。</p><p>总之,希望大家开心就好。</p><p>目前 Dark 已经不再是 Impart 的群管理了,只能算得上半个策划罢了。不过这个企划,我还是会坚持下去的。</p><h1 id="未完待续"><a href="#未完待续" class="headerlink" title="未完待续?"></a>未完待续?</h1><p>算了,实在是憋不住了,某些老师的某些课程分数还没出来,但是我还是等不及了。直接端上来罢 !</p><p>由于一些奇怪的原因,下半学期的 DLC 从 6 周延长到了 10 周,这多少令人会有点恼火。说实话,我是不喜欢暑假的时候做正事的。我希望能给自己的内心留下一个思考的时间,能让自己无拘无束的幻想…… 当然,我也能大致能猜到 yyu 这么做的原因啦 : 把编译器这个项目迁移到暑假,能让我们下学期的课业压力减轻,然后顺理成章的把之后的课程前移,这样可以大大加快整体进度,培养加速。当然,只是一个猜测罢了。</p><p>首先是长达 6 周的 PPCA。在前两周的时候,写了一个 Risc-V 的 CPU 模拟器。作为一个<del>毒瘤</del>coder,我的想法是迫真去模拟这个 CPU,因此用到了大量的 bit field (C 语言特性),用来压位,进而更加逼真地模拟 CPU 内部信息传输。包括指令等一系列的存储解析,我也是使用了结构体 + bit field 的方法来实现。虽然这么写的确废了我不少时间,差点还没写完,不过也的确让我能对其有一个更加深入的理解。不得不说,tomasulo 的设计确实精妙,乱序执行的正确性搞了我好久……现在真的觉得 CPU 流水真的是顶尖人类智慧。</p><p>然后是有趣的自选主题。Dark 选的是 Networking ,因为这是今年新出的项目,然后感觉非常有趣,于是就选了。通过这玩意,我接触了非常有意思的 go 语言 ,也接触了携程(goroutines)这个概念。go 写携程真的方便 ! 这简直就是为了携程而设计的语言 ! 在将近一个月的时间内呢,写了一些有趣的东西,比如 TCP 代理客户端,支持 UDP 的 DLC ,TLS 劫持以及 HTTP 的各种处理解析。确实,学到了很多我完全不会的东西,也真的让我觉得网络是一个非常有意思的东西,也见识到了网络的人类智慧之处。感觉现代科技真的处处是人类智慧啊……</p><p>不过要说令我印象最深刻的,那莫过于在 PPCA 的时候打过了《东方星莲船》的 Easy (是的,这是一款非常”简单“的游戏~) 。作为一个 STG 手残党,在某天偶然的摸鱼中掌握了开碟的技巧,然后瞬间打的无比通畅…… 不过这或许也和我的心态有点关系,在正常学期内的时候,我电动可没有现在这种悠闲的心态。总之,也不知道为什么,7 月 20 日下午,我就莫名过了星莲船 Easy 。这是值得纪念的一天,因为这意味着我打通了从永夜抄到鬼形兽(除花映冢)之间所有的正作 Easy 了 ! 或许,以后可以挑战挑战 Normal 了吧。</p><h1 id="编译器"><a href="#编译器" class="headerlink" title="编译器"></a>编译器</h1><p>编译器是个有意思的东西,写的是一个类 C 的 Mx 语言的 Compiler 。花了大概三周,完成了从 MX 到 AST 到类 llvm IR 到 ASM(on Risc-V 32bit) 的过程。</p><p>不过这几周,除了 Compiler 的实现,我倒是也干了不少其他的事情了。比如说混入某 <a href="https://github.com/Mq-b/Loser-HomeWork">Modern C++ Loser 群</a>,开始学 Modern C++ 了。这的确是一个有意思的群,我也的确从中学到了不少的 tricks。然后,我也的确有把部分所学用到 Compiler 代码里面啦,比如有趣的 intellisense-friendly 的字符串拼接函数:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="keyword">constexpr</span> <span class="type">size_t</span> __string_length(std::string_view __view)</span><br><span class="line">{ <span class="keyword">return</span> __view.<span class="built_in">size</span>(); }</span><br><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="keyword">constexpr</span> <span class="type">size_t</span> __string_length(<span class="type">const</span> <span class="type">char</span> *__str) {</span><br><span class="line"> <span class="type">const</span> <span class="type">char</span> *__tmp = __str;</span><br><span class="line"> <span class="keyword">while</span>(*__tmp) ++__tmp;</span><br><span class="line"> <span class="keyword">return</span> __tmp - __str;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="type">size_t</span> __string_length(<span class="type">const</span> std::string &__str) {</span><br><span class="line"> <span class="keyword">return</span> __str.<span class="built_in">length</span>();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="type">size_t</span> __string_length(<span class="type">char</span>) { <span class="keyword">return</span> <span class="number">1</span>; }</span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> ...T></span><br><span class="line"><span class="keyword">inline</span> <span class="type">size_t</span> __string_length_sum(<span class="type">const</span> T &...__args) {</span><br><span class="line"> <span class="keyword">return</span> (__string_length(__args) + ...);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Empty join will be invalid. */</span></span><br><span class="line"><span class="function"><span class="keyword">inline</span> std::string <span class="title">string_join</span><span class="params">()</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Check whether this type is string or char type. */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> ...T></span><br><span class="line"><span class="keyword">inline</span> <span class="keyword">constexpr</span> <span class="type">bool</span> __is_string_v = (</span><br><span class="line"> (std::is_convertible_v <T,std::string_view> </span><br><span class="line"> || std::__is_char <T> ::__value) && ...</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Join strings together , safe and fast ! */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> ...T></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">string_join</span><span class="params">(T &&...__args)</span></span></span><br><span class="line"><span class="function">-> std::<span class="type">enable_if_t</span> <__is_string_v <T...>,std::string> </span>{</span><br><span class="line"> std::string __ans;</span><br><span class="line"> __ans.<span class="built_in">reserve</span>(__string_length_sum(__args...));</span><br><span class="line"> (__ans += ... += std::forward <T> (__args));</span><br><span class="line"> <span class="keyword">return</span> __ans;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Join strings together , safe and fast ! */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">string_join_array</span><span class="params">(T __beg,T __end)</span></span></span><br><span class="line"><span class="function">-> std::<span class="type">enable_if_t</span> <__is_string_v <<span class="title">decltype</span> <span class="params">(*__beg)</span>>,std::string> </span>{</span><br><span class="line"> <span class="type">size_t</span> __cnt = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> __cur = __beg ; __cur != __end ; ++__cur)</span><br><span class="line"> __cnt += __string_length(*__cur);</span><br><span class="line"> std::string __ans;</span><br><span class="line"> __ans.<span class="built_in">reserve</span>(__cnt);</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> __cur = __beg ; __cur != __end ; ++__cur)</span><br><span class="line"> __ans += *__cur;</span><br><span class="line"> <span class="keyword">return</span> __ans;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>不得不感慨,真的处处是人类智慧。</p><p>当然,这段时间里,我也干了超级多 Compiler 之外的事情,这很不符合我 <del>致远卷王</del> 的一贯作风。其实也不是我不想学~卷不动了~,只是我不觉得这么内卷很有意义,而且我已经一个学期 + 6 周没有好好休息过了,我精神上也有点受不了。有的时候,特别是深夜 EMO 的时候,我也真的会怀疑自己是不是太菜了,是不是不适合这个班,就这点破意志力。但我还是坚信,还有更多有意义的事情等着我去做,如果找不到自己的本心所在,一昧地像以前那样不知道为啥去学,那我地内心最终也会麻木吧。而我,真的很不想,也很害怕自己变得麻木。如果现在都不能坚持自己的本心去做自己想做的事情,那么我有何理由去相信未来的自己能坚持自己所选的道路。</p><p>在这段时间里呢,我刷了不少东方相关的经典作品,主要以手书为主。在重新审视这些作品的时候呢,我也产生了不少新的感触,思考了许多第一次看的时候没去思考的东西。这种感觉在我两年前刚接触音频处理后,重新去听我曾经喜欢的音乐时也出现过,那种从熟悉事物中收获更多,如同从一个被榨干的柠檬中挤出更多的汁水的复杂情感(奇怪的比喻)。</p><p>这四周,压轴戏想必是 8 月 19 号和 20 号两天的 THO 了! 作为一个入坑将近 5 年的车万人,第一次去 THO ,自然是非常激动。而这短短的两天,也让我有了许多感触,打算单独开一篇文章来写。</p><a href="/SHTHO11/" title="第十一届上海 THO · 东方露明境 游记">第十一届上海 THO · 东方露明境 游记</a><h1 id="摸了"><a href="#摸了" class="headerlink" title="摸了?"></a>摸了?</h1><p><del>摸了,正在等学子讲坛 && PPCA 评分 ,不过说实话,时至今日,结果貌似对我没有那么重要,至少我感觉如此。不知道是我是真的不再期待当初那份努力的回报,还是我真的太过疲倦而麻木了。管他呢</del></p><p>一眨眼都摸到开学了,这就很尴尬了,已经没啥好评价的了,编译器也终于告一段落了,感兴趣的可以去看文章 <a href="/CompilerMx/"> 关于 Compiler Mx* 的随笔 </a> 。</p><p>不过在最后的最后呢,还是放送一些小故事和图片吧(笑),主要是之前 3 月份团日去参观了一大会址。那次活动结束后本来要乘大巴回学校,不过我突发奇想想要自己逛一逛,于是就开始了长达两个多小时的骑行之旅~</p><h1 id="回忆杀"><a href="#回忆杀" class="headerlink" title="回忆杀"></a>回忆杀</h1><p>大量图片警告!</p><p>出发点是交大医学院。</p><p><img src="https://s2.loli.net/2023/10/02/t3xjZhq2Ts7z6EW.jpg" alt="启动!"></p><h2 id="卢湾体育场-——-童年,理想,梦"><a href="#卢湾体育场-——-童年,理想,梦" class="headerlink" title="卢湾体育场 —— 童年,理想,梦"></a>卢湾体育场 —— 童年,理想,梦</h2><p>第一站卢湾体育场。不得不说变化还是有点的,塑胶跑道从红色变成了蓝色。这是笔者梦开始的地方,笔者从小学就一直在这里玩耍,也是笔者梦结束的地方,上了高中以后,我就很少去那里了,在搬家以后就再也没去过。梦,终有一天还是要结束的呐。</p><p><img src="https://s2.loli.net/2023/10/02/y7R3GJBqDirdvNY.jpg" alt="感慨万千,可惜童年玩伴大多再也联系不上了......"></p><p>第二站嘉善路地铁站,是我以前的家附近最近的地铁站。依稀记得在我小学五年级到初一的那几年,门口的嘉善路地铁站正在修新的地铁线路 12 号线,周围路基本被围起来了,出现略不方便。后来修好了以后,12 号线由于其贯穿市中心,能够直接换乘大部分线路,它一下子成为了上海地铁里面的 “原神” (奇怪的比喻),在一年的时间内变成了拥挤程度不亚于一号线的存在。</p><p>总之,在我生命的浪漫的前 16 年里面,我见证了这条地铁线路的诞生。在我搬家之前,嘉善路地铁站还是一个平平无奇的小站。我还记得我正常是从东边那个出口出来,然后往南走回家。或者从西边那个出口出来,那边有全家可以去买饮料。然后在 12 号线修好了以后,我也会从西南的那个出口出去,不过当时那边还没啥建筑,非常的空旷(从前貌似是一个古玩市场(?),大概有点回忆)。</p><p><img src="https://s2.loli.net/2023/10/02/iBmFbUvojH1K5T8.jpg" alt="以前还没有这个大楼,这几年一下子建起来了,甚至还多了个麦当当"></p><p><img src="https://s2.loli.net/2023/10/02/89i6VvLJPswaDlz.jpg" alt="好吧记错了,古玩城还在"></p><h2 id="回家-大木小区"><a href="#回家-大木小区" class="headerlink" title="回家! 大木小区!"></a>回家! 大木小区!</h2><p>下一站就是回 “家” 了 ! 对,就是回到那个我居住时间最长的家,从 2008 年到 2020 年,整整 12 年的时间,我都居住在那个安静的坐落于市中心的老旧小区里面。那个小区 —— 大木小区。其前身就有不少的故事,只可惜年代久远,这部分的记忆早已淡忘,可惜。目前,我只记得其有 3 个进出口,由看起来完全无关的 3 个部分构成了一个融合的 “中型” 居民区。</p><h3 id="正阳路"><a href="#正阳路" class="headerlink" title="正阳路"></a>正阳路</h3><p>从后街的那个门进入小区,简单逛了一下,暂时没找到什么大变化。除了墙皮刷新了以外,以前的电瓶车车棚什么的依然都在,那家坐落于角落的小卖部也健在(可惜忘记拍照了)。</p><p><img src="https://s2.loli.net/2023/10/02/174Plf9Vt3MhFIU.jpg" alt="后街正阳路,依然是这么的充满年代感"></p><p><img src="https://s2.loli.net/2023/10/02/u7RLMlWAz18HsEU.jpg" alt="后街的门,感觉重制以后门牌好看了不少"></p><h3 id="大木桥路"><a href="#大木桥路" class="headerlink" title="大木桥路"></a>大木桥路</h3><p>然后是偏向大木桥路的那一侧居民区,那边看起来变化也不是特别的大,也不知道曾经住在那边的几个童年玩伴,现在还在不在那里……不过那个小区内幼儿园居然还在,这是我没想到的(话说门口垃圾站这种逆天设计还没换吗qwq)</p><p><img src="https://s2.loli.net/2023/10/02/rXUDhj9B6Lm8TaI.jpg" alt="居民健身器材换了一套,不过还是在老位置"></p><p><img src="https://s2.loli.net/2023/10/02/QxG6dlV487qSJpc.jpg" alt="那个 inline 于小区的幼儿园还在哦,几乎没啥变化"></p><p><img src="https://s2.loli.net/2023/10/02/3Wm1HrlqO9t5e7i.jpg" alt="大木桥路的门"></p><h3 id="斜土路"><a href="#斜土路" class="headerlink" title="斜土路"></a>斜土路</h3><p>最后是偏向于斜土路的那一侧了! 那便是我以前居住的地方。依稀记得以前家对面就是橘子酒店,没想到现在居然还在。橘子酒店貌似就是在我小学五年级或者是预初初一时开的,我对其的印象时每天晚上都要开到很晚,灯还特别亮,直到 11 点后才关灯。然后隔壁的小巷子偶尔也会有人大喊大叫的,不过我睡得倒是很死……</p><p>这一块比起另外两个区域倒是有些明显的变化,许多区块都装电梯了。可以看到电梯井凸出来一块在外面,略影响外观,不过对于老年人来说,电梯还是能方便不少,特别是这种老小区有很多的老年人 (不过我当时见过的大部分老年人都非常健康的说,好多人手脚可能比我还利索,这下我变成老年人了(悲))。</p><p>令我非常吃惊的一点是,那个裁缝店,居然还在。印象里那个店老板娘的女儿还是儿子在南洋模范中学读高中,貌似考上了一个不错的大学,然后我妈和老板娘关系还不错,两人经常见面会聊聊天什么的。能够再次见到熟悉的人,心里还是又激动又欣慰的,只可惜我还是没能鼓起勇气上前打招呼,转眼间 3 年过去了,想必她也早已忘记了我了罢。</p><p><img src="https://s2.loli.net/2023/10/02/982ozv7mXAink43.jpg" alt="晾衣杆和桔子酒店"></p><p><img src="https://s2.loli.net/2023/10/02/Faw3nLP981tWqMR.jpg" alt="楼下视角,猜猜我住在几楼"></p><p><img src="https://s2.loli.net/2023/10/02/rjcVMqKO5BD1Isx.jpg" alt="裁缝店"></p><p><img src="https://s2.loli.net/2023/10/02/lfJGbZ2TajwEgVR.jpg" alt="店门上贴的,还是非常有深度的,但小朋友看不看得懂就不好说了"></p><p><img src="https://s2.loli.net/2023/10/02/fFMdHv4EpDo8hsJ.jpg" alt="斜土路的门"></p><h2 id="小插曲"><a href="#小插曲" class="headerlink" title="小插曲"></a>小插曲</h2><p>小插曲,当时我在朋友圈发了卢湾体育场的照片,结果发现 “丁枪” (某同学的“爱称”) 正好在附近买书,于是我与他在 12 号线大木桥路地铁站见面了,然后两人一起(其实是我拉着他)开始了 Dark 的重温童年之旅。</p><h3 id="小学-——-梦的种子"><a href="#小学-——-梦的种子" class="headerlink" title="小学 —— 梦的种子"></a>小学 —— 梦的种子</h3><p>笔者就读于建襄小学,一个笔者经常写错名字的小学。在这个小学里,笔者度过了充满波折但是无比快乐的 5 年。笔者永远不会忘记曾经和小伙伴们一起在里面捉迷藏的经历,也忘不了当年把排球打到了隔壁楼里面然后被老师痛骂的经历。笔者也记得曾经学校的南边的架子下面有一个马蜂窝(可能只是普通的蜂窝,但是当时看熊出没看多了就这么叫了),后来不知道怎么就没了。笔者也记得每次春游秋游和小伙伴们一起玩 mc 的经历。美好的回忆的碎片几乎是内嵌进了我的脑子里,即使是现在我也无法忘记,仿佛那一幕幕就在我的面前。</p><p>不得不说,我小学班主任 (或者是初中班主任) 有一句话说的很对: “珍惜当前的时光,当你回过头来看这一段时间,你会发现这是你人生中最快乐、轻松的一段的时光。” 现在看来,的确是这样的。时至今日,我依然保有一颗玩耍的心,但是却再也没有玩耍的时间和精力了,即使真的让我去玩,长大的我也一定会因为尴尬而拘谨,完全做不到无拘无束吧。童年的美好回忆大多来自于此。这颗十年前的子弹,飞了这么多年,终于打到了我自己的身上。</p><p>总是要亲自体验过了,才会懂得珍惜。可惜当时的我只知道玩,不过这样反而为我自己留下了一个几乎没被污染的童年,比起那些经常在外补课的人,我觉得我还是非常幸运的。</p><p><img src="https://s2.loli.net/2023/10/02/tm6Iai7QML2Pv8K.jpg" alt="校区变化不大,不过这里本来是给 3 ~ 5 年级的,现在貌似是 1 ~ 2 年级了"></p><h3 id="幼儿园-——-梦的温床"><a href="#幼儿园-——-梦的温床" class="headerlink" title="幼儿园 —— 梦的温床"></a>幼儿园 —— 梦的温床</h3><p>第二站是幼儿园。老实说,我已经完全快忘记关于这些东西了 …… 不过,不可否认的是,在早年接收到的关于爱、关于为人处事的那些教诲,其成为了我初中乃至是高中之前的“人生”信条。那些早年受到的传统教育,也在一定程度上塑造了我这个人的内在人格。说实话,幼年教育真的对我来讲影响深远。每当我反思自己,向内窥探自己的内心,我总是能发现一些曾经的自己的影子,即使在初中高中的时候我曾经无数次的推翻过去的那个自己,尝试走向反向的极端,但到头来,早年的教育还是给我留下了深深的,不可磨灭的烙印,在内心的深处的无意识部分,他们依然会出现 <del>(幻视一个共产主义的幽灵)</del> 。</p><p>在这里,我不会去评价这些教育的好坏,但是可以肯定地一点是,它真的可以说在很大程度上决定了我的人生轨迹。</p><p><img src="https://s2.loli.net/2023/10/02/DPTX3Bn4gJadFvA.jpg" alt="印象真的已经无比模糊了"></p><h3 id="初中-——-梦的启航"><a href="#初中-——-梦的启航" class="headerlink" title="初中 —— 梦的启航"></a>初中 —— 梦的启航</h3><p>初中,可以说是我真正起飞并且转型的地方。在初中之前,我从来都没想过我是怎么样子的一个人。我几乎没有什么竞争的意识,思想上也只是一个孩童罢了,只知道玩。即使家长逼着我去上课,但由于我一直强烈抵抗,所以最后基本都是不了了之了。</p><p>上了初中,我才第一次感受到了压力。那是来自升学的压力: 要考上一个好的高中,才能读一个好的大学,才能有一个好的人生。这大概就是我当时所想的,就是这么的简单朴素。至于为什么,我可从来没有去思考过。但是秉持着这样的信念,我开始了初中的学习生活。有的时候,生活进行下去或许只需要一个信念,对吧……</p><p>初中,我真的学了好多,也接触并深入了解了好多有趣的人,也真正的结交了不少的朋友 (天哪,上次同学聚会居然还有一位记得我当年的 Minecraft ID: DarkSharpness ,我真的差点哭出来)。说到初中,最骄傲的事情就是和某同学 LD 一起 “发明” 并且推广了一个手指游戏 “Finger Glory” ,当时因为一个契机手指游戏 + 职业的玩法火了起来,然后在大家的共同努力下,这个叫做 “Finger Glory” 的手指游戏便火了起来,并且在“良好”的运营下达到了动态的平衡。</p><p>当然 “Finger Glory” 主要是在初二初三的时候流行。在这之前两年,我可能还在某桥牌俱乐部打桥牌,以此作为我平时的消遣。当然,消遣,自然也少不了电脑上的 Minecraft。当时多玩我的世界盒子还没被干掉,我经常和一两位同学一起玩服务器。仍记得当时我在初一的时候每天回家第一件事情,就是趁着家长没回来,赶紧打开电脑,输入早已被我偷看到的密码,然后上服务器玩。当时受到大量台湾实况主的影响,特别喜欢原版生存(当然也会偶尔玩玩 mod ,比如暮色森林,工业 2 之类的比较热门的),和 jessy5610 (没错,我也记得你的游戏!) 一起在天籁服务器 (我记得是 tl.minemc.cn) 玩原版。虽然玩的真的很一般 (远不如高中的水平) ,但是真的也很开心。快乐就是简单而朴素的。</p><p>初三的时候,时间少了,但是消遣不能少。除了正常的 “Finger Glory”,我开始迷上了化学。说实话,我根本不是一个认真学化学的人,从现在的角度来看,我不过是喜欢化学实验那种直观而震撼的效果罢了,高中以后我的选择也证明了这一点。当然,迷上化学除了化学反应的新奇性,这背后也少不了两位化学老师的帮助。一位是课内的老师,另外一位是外面补课班的老师。我不知道怎么去形容,但是这两位老师成功给我打开了化学的大门,让我对其真的产生了浓厚的兴趣,也成为我喜欢化学试验的导火索。回到正题。初三的时候,我开始做一些化学试验,虽然基本以安全的为主,但偶尔也有危险的,比如电解制氯气(当时一晚上没关电没把我毒死真的是奇迹),以及学校里混合开水生石灰氯化铵(那次炸的很厉害,氨气直接冲的我眼睛都睁不开,幸好旁边就是厕所赶紧处理掉了,没出大事)。当时我可真的是不怕死,天真可爱。</p><p>所以简单的回顾了以下自己的初中,尽管我遇到了人生路上的第一道坎 —— 中考,它让我真正意义上有了学习的概念,也开始有了明确的奋斗目标(虽然我甚至不知道其意义何在),但它并没有让我改变太多,我依然如同孩童一样去观察这个世界与世界交互。当然,我也不后悔。这是我自己选择的,我也的确度过了一个快乐而完整的童年 (事实上升学的压力直到初二下我才真正有感受到),也真的第一次结交到了一些一辈子的知心朋友。</p><p><img src="https://s2.loli.net/2023/10/02/cJOgbtwz52o9RFy.jpg" alt="顺路拍的,以前一位初中好友 LD 就住在这里"></p><p><img src="https://s2.loli.net/2023/10/02/or1OxsFguvdH8zf.jpg" alt="徐家汇附近还是热闹啊"></p><p><img src="https://s2.loli.net/2023/10/02/YkTKXySlZ25UFDP.jpg" alt=""></p><p><img src="https://s2.loli.net/2023/10/02/ZirOYzS7m4R3gjw.jpg" alt=""></p><p><img src="https://s2.loli.net/2023/10/02/ESi2xo1jnNAML7F.jpg" alt=""></p><p><img src="https://s2.loli.net/2023/10/02/CB41YhL3VMsWUyA.jpg" alt="到达! 徐汇中学!"></p><h2 id="Ending"><a href="#Ending" class="headerlink" title="Ending"></a>Ending</h2><p>最后和 “丁枪” 同学在漕宝路地铁站告别了,然后我就独自骑回了上中附近(当时还住在那边)。感谢一路陪伴 !</p><p>距离这次的旅途,其实也过去半年多了。其实我也早就想要写了,奈何自己还是太摆了,加上大大小小的事情确实也不少,所以拖到了现在。只能说,这的确是一次非常有意义的旅途,不过当时我没想那么多的说,光顾着重温当年的那些美好回忆去了 (即使现在,坐在电脑前的我看到这些照片,心中依然会涌起一阵情绪) 。</p><h1 id="Ending-1"><a href="#Ending-1" class="headerlink" title="Ending"></a>Ending</h1><p>好了差不多该结束了,写到这里语言逻辑都已经错乱了。挑了几个比较有感触的活动写了写,自己写完也是非常激动的哈,希望不要只是单纯的自我感动,有机会还是多思考思考吧。</p><p>最后的最后,感谢您能阅读到这里! 如果你有什么想说的,欢迎在下面评论区留言,也算是支持一下我了,给我更多的动力。感谢!</p>]]></content>
<summary type="html">不知道该说啥,又是胡言乱语的一天</summary>
<category term="随笔" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
<category term="总结" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/%E6%80%BB%E7%BB%93/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>CP29 Day1 一日游</title>
<link href="http://darksharpness.github.io/CP29/"/>
<id>http://darksharpness.github.io/CP29/</id>
<published>2023-05-02T12:22:36.000Z</published>
<updated>2023-05-02T13:42:52.000Z</updated>
<content type="html"><![CDATA[<p>文笔不好,仅作个人游记,希望在互联网能有点存在感罢了。</p><p>多图预警! 有能力挂个梯子/加速器 qwq.</p><h1 id="入场"><a href="#入场" class="headerlink" title="入场"></a>入场</h1><p>前一天晚上就已经听闻了 CP29 的游客数量特别恐怖,于是一大早(其实 6:30 很晚了) 就起床从你交打车去了国家会展中心。</p><p>老实说,在进去之前,我都没想过会有这么多的人,少见的能在这么狭小的空间内见到这么多灵长类动物(笑)。</p><p><img src="https://s2.loli.net/2023/05/02/kiJ8hdAnarqWSeo.jpg" alt="下车等红绿灯ing"></p><p><img src="https://s2.loli.net/2023/05/02/yaBjAs3GT8zD9xO.jpg" alt="刚见到队伍"></p><p><img src="https://s2.loli.net/2023/05/02/Dm2ZdVPrej1pIou.jpg" alt="7:31,第一次到3,4区,记住!后面要考"></p><p><img src="https://s2.loli.net/2023/05/02/F3Yzm4MWrVePRSL.jpg" alt="开始发现不对劲的 DarkSharpness 和 abelcat"></p><p>小插曲: Dark 和 smarthehe 前后几乎只差了 2min ,但是最后 Dark 却比 smarthehe 晚进场了半个小时多。高中的大哥 (hjh同学) 比我晚到了 10 min,结果比我晚进场了快 2.5 h。所以合理推测: 早期排队 1min 人流量 = 15min 过安检人流量。难怪后面安检直接破防了,干脆不查了直接放人进去了。</p><p><img src="https://s2.loli.net/2023/05/02/ARgG3LXDdY5Tm19.jpg" alt="8:23,此时刚排队20min,问就是排错了一次,队尾太不明显了"></p><p><img src="https://s2.loli.net/2023/05/02/Sz8wL1ojHDR9mbA.jpg" alt="8:51,才刚到天桥下"></p><p><img src="https://s2.loli.net/2023/05/02/8LJfUnYmubkK2CP.jpg" alt="9:04,回到了开始的地方"></p><p>小插曲: 在一路上见到了许多的东方爱好者和 coser,感觉都好可爱捏。作为一个前莉莉厨,特地拍下了这一只令人印象深刻的,大只莉莉白!</p><p><img src="https://s2.loli.net/2023/05/02/i8DC5OGUMLoSHc1.jpg" alt="9:15,大只莉莉白"></p><p><img src="https://s2.loli.net/2023/05/02/XtIPQopSzbefalk.jpg" alt="9:24,希望人没事,身体第一(虽然Dark也是嗑药硬撑)"></p><p><img src="https://s2.loli.net/2023/05/02/ofENDUQiAuTBICV.jpg" alt="9:28,过转角,发现内侧道路显著快于中外"></p><p><img src="https://s2.loli.net/2023/05/02/9CAOnwh1LpM683g.jpg" alt="10:20,终于到一楼缓冲区了,death rush(错乱)!"></p><p><img src="https://s2.loli.net/2023/05/02/FWJR4NDz5yliYtP.jpg" alt="10:21,我测,这么密集,玉玉了"></p><p>接下来是 Dark 最破防的一段时间,smarthehe 已经进去了,而我面前还有一个漫长的队伍,甚至没见动弹。雪上加霜的是,旁边的保安放人规则很抽象,导致我们后面有些人反而跑到前面去了。说实话,在来之前,我已经预料到了会有此类小小的不公平的现象,我也已经打好预防针绝对不会急。然而实际利益受损,加上各种别人比你更优,我还是 pdf (破大防) <del>(泼大粪,指模拟野兽先辈发出怒吼)(下次可以试试看cos 先辈)</del> 了。</p><p>好在后面队伍移动还是很快的,心态马上平衡了。在经过了体感一亿年的等待后,终于上坡了(从一楼缓冲区走向二楼)。看着坡下的人,还是感慨万千捏。在大约一个小时多前,我也是下面的一员,憧憬着美好的漫展 <del>(Touhou-only)</del> 。不过,我还是不想要像之前之前坡上某些人那样,嘲讽下面人,这样确实不太好,还是不要把自己的负面情绪发泄到他人身上。<del>(先辈叫不算发泄到他人拉,是一起发泄)</del></p><p><img src="https://s2.loli.net/2023/05/02/e9Ms61jEgK5tcb7.jpg" alt="10:50,终于上坡了"></p><p><img src="https://s2.loli.net/2023/05/02/Yi83gHKvBVDydtG.jpg" alt="10:51,上坡时候激动的人群"></p><p><img src="https://s2.loli.net/2023/05/02/wgf6rMZz1Wxo3m9.jpg" alt="10:52,隔着围栏也能看到里面的情况"></p><h1 id="游览"><a href="#游览" class="headerlink" title="游览"></a>游览</h1><p>我的游览过程很无聊,主要想看看 LL(love live) , Vocaloid 和 东方 展区,买完核心后瞎逛逛。然而,最后还是基本集中在了东方展区,后面也将着重介绍车万展区。</p><p>由于笔者线上社牛但线下社恐,所以没敢找 coser 拍照或找人搭话,一直被人忽略,也没敢拍几张照片 (毕竟展区说了不让拍,我是个守规矩的公民捏)。不过摊主和游客们的强烈热情还是不难感受到的。</p><p><img src="https://s2.loli.net/2023/05/02/Yj1Fu2396dkypav.jpg" alt="11:00,到达展区!!!"></p><p>第一个逛的就是车万展区。不得不说你车居然还能独占一个巨大的区域,真的是挺出乎意料的的,期待 THO 会比现在人更多。</p><p>进场后直奔壹甲 60 二重不眠症的摊位。说实话,作为一个从他们建队就开始听的老粉丝,也确实应该买点实体专支持一下了,总不能一直在网易云音乐白嫖吧(笑)。最后买了 <a href="https://music.163.com/album?id=145225520">Helicon</a> 一张专,还顺带买了个鼠标垫(正好,不用买新的了),以及两个拨片(话说我又不会吉他,为啥要买捏?)。期待新专 + 演出。</p><p><img src="https://s2.loli.net/2023/05/02/EPxg4DoTsn1yeLM.jpg" alt="11:20 收下了!"></p><p><img src="https://s2.loli.net/2023/05/02/qpMoRw79AZg1W6r.jpg" alt="晚上开箱! 孩子很喜欢"></p><p><img src="https://s2.loli.net/2023/05/02/zkOx9XrG6taZFUn.jpg" alt="鼠标垫很漂亮,就是阻力有点大,需要适应下(笑)"></p><p>随后逛了逛其他东方的店铺,主要帮同学和自己买了些周边,以及四个抱枕😍。当时从kk的店里面出来,感觉路人看我如看变态一样(笑),<del>本来就是呀</del>。笑死了,想太多了,根本没人在乎你。</p><p><img src="https://s2.loli.net/2023/05/02/97bwUCFkBAEqQDr.jpg" alt="特地克制了点,避免陷入消费主义的陷阱"></p><p><img src="https://s2.loli.net/2023/05/02/5tq3VvRhacuOGbw.jpg" alt="抱枕,但两个是 hsfzLZH1 的"></p><p>然后帮 zhuoyue 买了些 V 周边,顺便逛了逛。笔者对 V 一直处于感兴趣,但是不深入坑的边缘状态,也不知道为什么,只有车万一下子就把我拉下水了。或许是车万的幻想属性,抓住了我大脑一片空白的那段时候(初三),抑或是一个意外罢了。</p><p><img src="https://s2.loli.net/2023/05/02/a4qbifPVecUT1k8.jpg" alt="12:24"></p><p><img src="https://s2.loli.net/2023/05/02/eljnpcLKV2h4Y9Q.jpg" alt="看起来少,但是好™的重啊"></p><p>小插曲,见到了囧仙本人。由于太社恐,只敢偷拍捏。囧仙可爱捏,不过感觉好大一只,感觉三个我都打不过(误)。</p><p><img src="https://s2.loli.net/2023/05/02/1tgKd4NJUAwl2Za.jpg" alt="12:34,😍"></p><p><img src="https://s2.loli.net/2023/05/02/QL2dxbZETyIcS5t.jpg" alt="12:51,又见"></p><p>后面就是和 smarthehe 和他的同学们,以及我可爱的高中同学屌哥,大哥,MisakaVan,鼎汉,tzy 等人面基了。可惜的是照片没拍,不过最美好的回忆已经存在脑子里了捏 (人体硬盘)。</p><h1 id="润"><a href="#润" class="headerlink" title="润!"></a>润!</h1><p><img src="https://s2.loli.net/2023/05/02/IXGMziuDvtB63hY.jpg" alt="全部货物"></p><p>总消费控制在了 250 以内,取得阶段性胜利。不过帮同学搬运东西还是挺累的,东西好重,幸好包装得下,我也习惯了包里面一堆重物。</p><p>总结不想写不会写,因为作者阅历不足,头脑单线条。总之,各位辛苦了,Dark 玩的很开心,终于能支持一波自己喜欢的一些同人作者了捏,也终于感觉到真正地融入到了东方同人这个群体中,有参与感了捏。</p>]]></content>
<summary type="html">关于 CP29 Day1 游览的一些个人情绪。</summary>
<category term="随笔" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>关于 Minecraft Impart 服务器</title>
<link href="http://darksharpness.github.io/mcImpart/"/>
<id>http://darksharpness.github.io/mcImpart/</id>
<published>2023-04-09T08:12:36.000Z</published>
<updated>2023-06-05T06:56:29.000Z</updated>
<content type="html"><![CDATA[<p>2023 年 2 月,大一下学期刚开学。由于开学实在是太无聊了,于是在一群同学商量之后,便有了 Minecraft Impart 服务器。原始 idea 由 photoshopcc 和 chayso 想出 (疑似),而服务器主体由 photoshopcc 维护 (其实就是租了一个服务器)。</p><h1 id="Impart-0-周目"><a href="#Impart-0-周目" class="headerlink" title="Impart 0 周目"></a>Impart 0 周目</h1><p>在最初的时候,Impart 只是一个生存 only 的 minecraft 服务器。当然,单纯的原版生存实在是太简单了,于是笔者便提议在原版的基础下做出了如下这些的改动:</p><ol><li>死亡掉落,而且保持永夜。</li><li>允许死后 tp 回到死亡位置捡尸。</li></ol><p>一开始,大家都玩的很开心。大家没有很强地目的性,只不过在随便跑跑图、杀杀怪物、偶尔下矿之类的,并没有很一个明确的目标(比如打龙或者pvp竞争),大家也都玩的很开心。不过如此单调的游戏玩法还是持续不了太久,大概就过了1 ~ 2 天,大家就决定干点有意义的事,例如去打龙。在众人的努力下,龙很快就被打掉了,而没过多久,大家也都拿到了鞘翅。</p><p>问题再次出现: 在打完龙以后,还有什么有意义的事情值得我们去做吗 ? 这个问题其实不仅仅发生在 Impart , 在千千万万个服务器里面,玩家或多或少地都会遇到这样的问题。在打完龙、杀完凋、造完自己温馨的小家之后,你是否还有想过之后的路该怎么走 ? Minecraft 是一个极其开放的游戏,而也正因如此,玩家面多无限的选择的时候往往会变得不知所措。</p><p>笔者 DarkSharpness 的建议是去搞生存电路,研究红石电路之类的,但是这实在是太肝了,再加上后面两周学业负担逐渐加重,导致了 Impart 服务器最终关闭,Impart 0 周目至此落下帷幕。</p><p>这次企划只持续了短短一周,便在不知所措中草草收尾。笔者也由此不禁开始反思,究竟是 Minecraft 里面的什么吸引着我一直去玩它 ,而又是为什么,我们会不再被其所吸引 ? Minecraft 本身是一个极其开放的游戏,其有着几乎无限的可能。然而,许多玩家在通关以后却会茫然不知所措。笔者认为,这背后其实是一个游玩目的缺失。围绕着游戏的主线 —— 生存和成就 去玩的确是一种常见的玩法,但是主线任务是有限的,这相当于把游戏无限的可能局限在了一个很小的范围,自然容易让人厌烦。</p><h1 id="Impart-1-6-周目"><a href="#Impart-1-6-周目" class="headerlink" title="Impart 1 ~ 6 周目"></a>Impart 1 ~ 6 周目</h1><p>由于缺乏足够的记录 <del>其实笔者懒得翻记录,哼</del> ,所以前几周的活动就没具体记录了haha。</p><p>新的 Impart 企划 : EHC (Easy hardcore),由笔者 (DarkSharpness) 发起,灵感来源是台湾实况主的那些 <a href="https://www.bilibili.com/video/BV1JW41137Jn/">UHC 活动</a> ,说起来也是挺怀念的。主要规则类似 UHC ,不过由于大家都太菜了,为了更好的游戏体验所以变成了允许回血(后续甚至允许复活)。具体规则看最后的附录。活动时间固定在周末前一天的晚上,当然其他时候偶尔也有加赛。</p><p>前几周大家都玩的非常不熟,经常会出现前 10 分钟就死一片的现象,也出现过各种搞笑的失误,比如珍珠飞出边界被挤压死、矿洞被僵尸灭队、被细雪干死、被猪灵蛮兵干死之类的。由于大家打的都挺菜的,所以玩的也很开心。</p><p>中间几周,大家都逐渐熟练起来,打法也逐渐趋向稳定。常见打法有直冲钻石层、矿洞速破、鱼骨到死,这个打法主要是 photoshopcc 和其他大部分人在用,效率挺高,不出意外的话,一个小时半可以钻石全套。DarkSharpness 本人一般喜欢走地狱打法,因为 Dark 本人之前玩过些速通 (玩的一坨答辩) ,对于地狱比较熟 (貌似基本上除了 Dark 以及其队员也没人去地狱),所以敢于大胆冲。这种打法略吃运气,需要通过猪灵交易获得大量的光灵箭和丝,如果没有猪堡会非常难受。当然,自从 photoshopcc 和 xun_ying123 等人某次被 Dark 的装备压制打到绝望以后,大家也都意识到了装备压制的重要性,因此开始注重堆附魔,甚至是钓鱼等附魔书。</p><p>当然,中间几周,我们也尝试了 bingo 这个玩法,前几次 bingo 玩的还是很开心,不过玩多了确实容易腻。就我个人而言,我还是喜欢吃中长期策略发育的 EHC,这更加不吃运气,考验的是发育速度和原版游戏理解。</p><p>在第 4 周或是第 5 周的时候,一件事情极大的改变了游戏的进程 —— 重生锚。是的,Dark 凭借多年的水晶<del>外挂端</del>pvp的经验,拿出了原版大杀器,重生锚。其伤害在困难模式下可以稳定秒杀正常发育下任意装甲的人。在这周及后面一周的比赛中,重生锚两次出现,直接将 3 个满血玩家直接炸死,这惊动了我们所有人。考虑到这个东西实在是太破坏原版 pvp 的趣味性了,所以,在商榷之后,我们决定在 60min(即不能复活后) 为每个玩家发放一本爆炸保护 IV 的附魔书,并且将游戏难度下调至 normal。</p><p>最后两周,离谱的事情接踵而至,首先是我和 photoshopcc 各自拿到了一个附魔金苹果,然后 photoshopcc 又找到了一本力量 V 的附魔书,而最后一次甚至我和 photoshopcc 各自找到了一个古城 (虽然我这个古城穷成伞兵)。考虑到这些都是小概率事件,其实也不是不能接受,但是离谱还是真的离谱。</p><p><del>由于星穹铁道的影响,服务器快倒闭了,没玩家了,速来 qq 群: 808813297</del></p><h1 id="Impart-的未来-何去何从"><a href="#Impart-的未来-何去何从" class="headerlink" title="Impart 的未来,何去何从"></a>Impart 的未来,何去何从</h1><p>现在是 2023 年 6 月 4 号。Impart 已经顺利地度过了前 11 周目。目前,服务器的 EHC 项目的指令部分已经就几乎完成,经过了两周的测试之后也基本稳定。本学期基本已经结束,Impart 的活动恐怕也要告一段落了。</p><p>在暑假,由于愚蠢的 PPCA 项目,我们将不得不在学校待到 8 月份。好消息是,伴随着 <a href="https://github.com/xunying123">xun_ying</a> 的装机,我们暑期服务器有着落了,平时可以开着台式电脑,作为常用服务器。</p><p>因此,DarkSharpness 首先的规划是开一个中长期的生存档,具体内容可以是原版养老,也可以玩红石(生存电路/数字电路),也可以玩玩建筑和跑酷什么的。当然,既然暑假有一点时间,Dark 也考虑过玩某些大型的 pve 地图,例如 Terra Restore。说起来,它可是承载了我半个初中的回忆啊,那时候一下课就回家打开电视,看搬运的舞秋风的大地复苏(Terra Restore)的视频。当然,大型的景观类生存地图,也是很不错的啦!</p><p>然后呢,Dark 还计划开一个小游戏项目。除了 Impart 的保留节目 EHC,可以重拾 manhunt,bingo,parkour tower 这些经典项目。当然,Dark 也想引入一些更加新颖的小游戏,例如烟花弩 pvp,又比如说mc fps(快速填充 V 的秒射箭),或者是火球 pvp,鞘翅 pvp 等等……</p><p>值得一提的是,在这段时间里面,我也认识了 lytDark,另外一个服务器的腐竹。他们服务器也没人,所以现在有在打算合并(真的不是吞并吗)两个服务器啦……</p><p>总之,期待一下新的 Impart !</p><h1 id="Rules"><a href="#Rules" class="headerlink" title="Rules"></a>Rules</h1><p>具体规则请转向 <a href="https://github.com/DarkSharpness/DarkSharpness/blob/main/Game/Minecraft/Impart/rules.md">Github</a>。</p><p>如有问题或补充,欢迎在评论区留言!</p>]]></content>
<summary type="html">Minecraft Impart 服务器的历史,以及活动策划的心路历程回忆录。</summary>
<category term="Minecraft" scheme="http://darksharpness.github.io/categories/Minecraft/"/>
<category term="服务器" scheme="http://darksharpness.github.io/categories/Minecraft/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
<category term="Minecraft" scheme="http://darksharpness.github.io/tags/Minecraft/"/>
</entry>
<entry>
<title>二项堆复杂度报告</title>
<link href="http://darksharpness.github.io/BinoHeap/"/>
<id>http://darksharpness.github.io/BinoHeap/</id>
<published>2023-03-15T00:02:16.000Z</published>
<updated>2023-03-15T07:30:12.000Z</updated>
<content type="html"><![CDATA[<p>前言: 二项堆复杂度分析是真的水。由于 DarkSharpness 实在是太懒了,所以图片全部来自 Wikipedia(所以要挂梯子).</p><p>先立个 Flag ,不用势能分析<del>(才不是不会呢)</del></p><p><strong>请不要用深色模式浏览本页,请点击右下角设置切换,否则图片会看不清</strong></p><p><strong>请不要用深色模式浏览本页,请点击右下角设置切换,否则图片会看不清</strong></p><p><strong>请不要用深色模式浏览本页,请点击右下角设置切换,否则图片会看不清</strong></p><h1 id="二项树"><a href="#二项树" class="headerlink" title="二项树"></a>二项树</h1><p>二项树如下递归定义:</p><p>$0$ 度的二项树只有 $1$ 个节点</p><p>$k(k \gt 0)$ 度的二项树包含 $1$ 个根节点,根节点下面有度数分别为 $k-1,k-2,\dots,0$ 的二项树,如图是一颗度数为 $0 ~ 3$ 的二项树:</p><p><img src="https://upload.wikimedia.org/wikipedia/commons/c/cf/Binomial_Trees.svg" alt="(来自wikipedia)"></p><p>不难发现,一颗度数为 $k (k \ge 0)$ 的二项树有恰好 $2^k$ 个节点。也不难发现,合并两颗度数为 $k (k\ge 0)$ 的二叉树,我们只需将其中一颗树连接到另外一棵树的根节点下面,便可以得到一颗度数为 $k + 1$ 的树,理论上一次 merge 只需要最坏 $O(1)$ 的时间。</p><h1 id="二项堆"><a href="#二项堆" class="headerlink" title="二项堆"></a>二项堆</h1><p>二项堆是基于二项树实现的,每颗树维护一个堆结构,节点之间的连接可以用链表实现。本文默认分析的是小根堆,大根堆同理。<del>还是不会就爬。</del></p><p><strong>*特别提示: 本文提到的常数指的是指 复杂度表达式 f(n) 前面的系数,例如复杂度f(n) = O(n) 常数为 2 指的是</strong></p><script type="math/tex; mode=display">\lim_{n\rightarrow\infty} \frac{f(n)}{n} = 2</script><h2 id="基本性质"><a href="#基本性质" class="headerlink" title="基本性质"></a>基本性质</h2><p>对于一个二项堆,其存储了一些二项树,满足这些树的度数两两不相等,且每颗树及其子树满足堆结构,即节点是树中值最小的节点。</p><p>注意到一个度数为 $k$ 的二项树有恰好 $2^k$ 个节点,所以假设当前二项堆节点数量的二进制表示为 $\sum_{i=0}^n {a_i \cdot 2^i} (a_i = 0 \ or \ 1)$ (后文用 ${ a_n }$ 简记),那么当前二项堆有一颗度数为 $i$ 的子树,当且仅当 $a_i = 1$ 。例如一个有 11 个节点的二项树,由于 11 的二进制表示为 $11 = (1011)_2$ ,那么这颗二项堆恰有度数为 $0,1,3$ 的三颗子树。</p><p>显然地,一个有 $n$ 个节点的二项堆,其含有的二项树的数量最多为 $\log_2(n) + 1$ 个,二项树的度数也不会超过 $\log_2(n)$.</p><h2 id="合并两棵树"><a href="#合并两棵树" class="headerlink" title="合并两棵树"></a>合并两棵树</h2><p>合并两棵树和之前提到的 merge 操作基本一致,不过需要特别注意,由于每颗树维护的是堆结构,因此我们需要把根节点值更大的树的根节点 连接到 根节点值更小的根节点下面,此时不难验证依然满足堆的性质。如下所示:</p><p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/Binomial_heap_merge1.svg/800px-Binomial_heap_merge1.svg.png" alt="合并节点"></p><p>显然地,该操作的最坏时间复杂度为 $O(1)$,常数取决于一次值比较所需的时间。</p><p><strong>我们称之为 merge_tree 操作</strong>.</p><h2 id="合并两个堆"><a href="#合并两个堆" class="headerlink" title="合并两个堆"></a>合并两个堆</h2><p>合并两个堆的过程类似两个整数的加法过程:</p><p>假设两个堆节点数的二进制表示分别为 ${a_n},{b_m}$ (特别地,对于 $i \gt n$ ,$a_i$ 记为 0),那么合并过程大致如下,类似正整数加法:</p><p>首先合并两个二项堆 度数为 0 的二项树。如果当前只有 0 或 1 颗 度数为 0 的二项树,那么保留当前的 0 或 1 颗树,不用合并。如果当前有 2 颗 度数为 0 的二项树,那么合并为 1 颗 度数为 1 的二项树。此时,我们认为,合并完了两个二项堆度数 $< 1$ 的二项树。</p><p>然后,假设已经合并完了两个二项堆 度数 $ < k$ 的 二项树,那么此时,最多会有 1(第一个堆) + 1(第二个堆) + 1(下面合并产生的) 颗度数为 $k$ 的二项树 。类似地,当度数为 $k$ 的二项树总数为 0 或 1,那么什么都不做。如果总数为 2 或 3,那么随机合并其中两颗,合并出一颗度数为 $k + 1$ 的树。此时,合并完成了两个二项堆度数 $ < k + 1 $ 的二项树。</p><p>不难发现,只有当两个堆的节点数,在从低到高二进制加法中出现进位的时候,才会执行 merge_tree 操作。例如当一个节点数为 7 的树和 一个节点数为 5 的树合并,我们首先写出两者的二进制表示 $7 = (111)_2 \ ,\ 5=(101)_2 $。因此其执行的合并操作如下:</p><p>首先合并两个堆的 2 个度数为 0 的两颗二项树,得到 1 个 度数为 1 的二项树。再将其和第一个堆度数为 1 的二项树合并,得到一个度数为 2 的 二项树。现在,我们得到了三颗 度数为 2 的二项树,我们随机合并其中两颗,保留剩下那颗度数为 2 的二项树,得到一颗度数为 3 的二项树。最后,我们只有一颗度数未合并,而单独一颗无需处理。至此,操作结束,得到了 1 颗度数为 2 的二项树 和 一颗度数为 3 的二项树。再对比两个数字的二进制加法,最低位置 1 + 1 进位 一个 1 ,留下的是 0 。第二位 1 + 0 + 1(进位) ,进位一个 1 ,留下一个 0 。第三位 1 + 1 + 1(进位),进位一个 1 ,留下一个 1 。最后最高位保留一个 1,结束计算。</p><p>很直观地可以看出,合并的次数等于节点数二进制加法时进位次数,而这样的操作最多会执行 $\log_2(n + m)$ 次。</p><p>因此,该操作的最坏时间复杂度为 $O(\log(n + m))$,常数取决于一次值比较所需的时间。</p><p><strong>我们称之为 merge_heap 操作</strong>。</p><h2 id="查询最小值"><a href="#查询最小值" class="headerlink" title="查询最小值"></a>查询最小值</h2><p>为了便于复杂度证明以及后续展开,这里先分析查询最小值的时间复杂度。</p><p>查询最小值,一个朴素的办法就是将每颗树的值最小的节点 (即根节点) 两两比较,假设当前二项堆节点数为 $n$ ,那么最多比较次数不会超过 $\log_2 n$ ,即查询最坏复杂度为 $O(\log n)$ ,常数取决于一次值比较所需的时间。</p><p>这样的实现显然是不友好的。事实上,我们可以进行针对性优化,我们保留一个指向最小值的指针。</p><p>每次插入一个节点的时候,我们只需额外比较一次插入值和当前最小值的大小,指针指向更小的那一个即可。这会给插入一个节点带来 $O(1)$ 的额外开销,常数取决于一次值比较所需的时间。</p><p>每次删除一个最小值节点的时候,我们等删除结束后再用朴素方法维护得到最小值的指针即可。这会给删除一个节点带来 $O(\log n) $ 的额外开销,常数取决于一次值比较所需的时间。</p><p>此时,查询最小值只需访问一次指针即可,查询最坏复杂度为 $O(1)$ 。</p><p><strong>我们称之为 top 操作</strong>。</p><h2 id="删除最小值节点"><a href="#删除最小值节点" class="headerlink" title="删除最小值节点"></a>删除最小值节点</h2><p>由前面的查询最小值操作,我们多维护了一个指向最小值的指针,从而避免了查询最小值的高开销,将其部分均摊到了插入和删除操作上。</p><p>借助这个指向最小值的指针,我们可以按如下方法实现删除最小值节点:</p><p>最小值节点必然是一颗二项树的根节点,我们将这个二项树从二项堆中拿出来,得到一个由单独的二项树构成的二项堆,和原来的二项堆。假设原来的二项堆有 $n$ 个节点,我们新建的二项堆含有的是一颗度数为 $k (k \le log_2n)$ 的二项树。</p><p>此时,我们删除新生成的二项堆的根节点,由定义,断开该节点相连的边以后,我们会得到度数分别为 $k-1,k-2,…,0$ 的 k 颗二项树,将其记为最新的二项堆。此时,我们将最新的二项堆和旧的二项堆合并即可,即可以用 merge_heap 操作解决。</p><p>最后,别忘了还有更新最小值指针的 $O(\log n)$ 的额外开销。</p><p>因此,由前面的分析,我们容易知道,删除所需的最坏时间复杂度为 $O(\log (n + 2^k) ) + O(\log n) = O(\log n)$ ,常数取决于一次值比较所需的时间。</p><p><strong>我们称之为 pop 操作</strong>。</p><h2 id="插入一个节点"><a href="#插入一个节点" class="headerlink" title="插入一个节点"></a>插入一个节点</h2><p>插入一个节点的过程简单来说如下:</p><ol><li>将单节点作为 仅含一个度数为 0 的二项树 的二项堆。</li><li>合并当前堆和新生成的二项堆。</li></ol><p>由前面对于合并堆的分析,我们不难看出,该操作所需的 merge_tree 操作次数取决于 $n$ 和 $1$ 做加法时候的进位次数,最坏时间复杂度为 $O(\log n)$ ,常数取决于一次值比较所需的时间。</p><p>但是若仅仅只有插入操作,那么假设一开始的节点数量为 $n$ ,进行了 $k$ 次连续的插入操作。分析单次加法操作,设 $x$ 和 $y$ 进行一次加法,$x$ 的二进制表示数位和 (后简称为数位和) 为 $s_x$ ,$y$ 的数位和为 $s_y$,若加法中进行了 $z$ 次进位,那么最后 $x + y$ 的数位和为 $s_x + s_y - z$ 。</p><p>因此从 $n$ 每次加一加到 $n + k$,设 $n$ 的数位和为 $s_1$,设 $n + k$ 的数位和为 $s_2$ ,而中间 $k$ 次加一,数位总和为 $k \times 1 = k$,因此进位次数为 $k + s_1 - s_2$ ,而易知,$s_1 \le \log_2 (n) + 1$ 。</p><p>因此,从节点数 $n$ 开始,连续插入 k 次的最坏时间复杂度为 $O(\log n + k)$ ,叠加上维护最小值的开销 $O(1)$,单次操作的平均最坏复杂度为</p><script type="math/tex; mode=display">O(\frac {\log {n}}{k} + 1) + O(1) = O(\frac {\log {n}}{k} + 1)</script><p>当 $k$ 足够大,或者初始 $n$ 足够小,或者 $k$ 和 $\log n$ 同一个数量级,那么单次只需要均摊 $O(1)$ 次的 merge_tree 操作 ,单次复杂度可以降低到 $O(1)$ 。</p><p>因此,单次插入操作的均摊复杂度为 $O(1)$ ,常数取决于连续插入次数 $k$ 、初始节点数 $n$ 以及一次值比较所需的时间。</p><p><strong>我们称之为 push 操作。</strong></p><h2 id="减小最小值"><a href="#减小最小值" class="headerlink" title="减小最小值"></a>减小最小值</h2><p>直接简化为一次删除 + 一次插入即可。易得最坏时间复杂度 $O(\log n)$,常数取决于一次值比较所需的时间。</p><p><strong>我们称之为 decrease_key 操作。</strong></p><h2 id="其他操作"><a href="#其他操作" class="headerlink" title="其他操作"></a>其他操作</h2><p>拷贝构造函数,可以通过遍历一遍节点,在 $O(m)$ 的时间内完成,其中 m 为被拷贝的二项堆的节点个数。。</p><p>析构函数,只需遍历一遍每个节点,在 $O(n)$ 的时间内完成,其中 n 为被拷贝的二项堆的节点个数。</p><p>拷贝赋值函数,可以通过先析构当前函数,再拷贝构造来实现。这样实现的时间复杂度为 $O(n + m)$ ,其中 n 为当前二项堆的节点个数,m 为被拷贝的二项堆的节点个数。</p><p>*如果实现了移动构造函数 (after C++ 11),那么移动构造只需移走待移动对象的指针,所以时间复杂度为 $O(1)$ 。类似地,移动赋值函数只需在原地移动构造前先析构当前函数,因此其时间复杂度为 $O(n)$ (仅析构)。</p><h1 id="就这"><a href="#就这" class="headerlink" title="就这?"></a>就这?</h1><blockquote><p>就这? 你是啥fw? —— DarkSharpness</p></blockquote><p>如果在 push 中,夹杂了一些 pop / decrease_key / merge_heap 操作 (显然穿插 top 是只读函数,不改变堆的结构,不会影响均摊复杂度),那还能保证单次插入均摊时间复杂度为 $O(1)$ 吗? 当然可以,回到之前的定义,初始 $n$ 个节点,连续插入 k 次的最坏时间复杂度为 $O(\log n + k)$ 。</p><p>*后文 merge 若无特殊指明,指代 merge_heap</p><h2 id="Decrease-key"><a href="#Decrease-key" class="headerlink" title="Decrease-key"></a>Decrease-key</h2><p>对于穿插的一次 decrease_key 操作,其本质上也不会改变堆的元素数量,拥有某个度数的二项树的情况和操作之前完全一致 (例如原本 3 个节点,只有度数为 0 和 1 的二项树,drecrease_key,那么新的二项堆,也只有度数为 0 和 1 的二项树)。因此,对于 push 的均摊分析完全没有影响。</p><h2 id="Pop-only"><a href="#Pop-only" class="headerlink" title="Pop-only"></a>Pop-only</h2><p>先不考虑 merge ,只考虑 pop。对于穿插的一次 pop 操作,设当前节点数为 $m$,若这次操作前面或之后存在一次 push 是节点数 $m - 1$ 增长到 $m$ ,我们可认为两次操作抵消掉,并且由于 push 单次最坏时间复杂度为 $O(\log n)$ ,我们可以把 push 的时间复杂度分摊到相抵消的 pop 上,pop 的最坏时间复杂度依然是 $O(\log n) + O(\log n) = O(\log n)$ ,但此时对应 push 的复杂度分摊给了 pop 降低为 O(1)。容易证明:</p><ul><li>当 push 次数不超过 pop 的次数的时候,每次节点数从 $m - 1$ 上升为 $m$ 的 push 操作必然会对应一次节点数从 $m$ 下降的 $m - 1$ pop 操作。此时,每次 push 的复杂度都分摊给了 pop ,因此 pop 依然是最坏 $O(\log n)$ ,但是 push 降低为了均摊 $O(1)$。</li><li>当 push 次数超过 pop 的此时,每次节点数从 $m$ 下降的 $m - 1$ pop 操作必然会对应一次节点数从 $m - 1$ 上升为 $m$ 的 push 操作,假设节点数从 $n$ 上升到了 $n + t$,push 了 $k = t + d \ (d \ge 0)$ 次,即有 d 次 抵消操作,那么所有 push 的最坏总复杂度为 $O(\log n + t) + d \times O(1) = O(\log n + k)$ ,类似之前 push 的分析,易得 pop 依然是最坏 $O(\log n)$ ,push 均摊 $O(1)$ 。</li></ul><p>总之,混杂了 pop 以后,push 操作依然可以均摊 $O(1)$ ,不过是把部分复杂度摊给了 pop 罢了。</p><blockquote><p>哪有什么岁月静好,不过是有人替你负重前行。</p></blockquote><h2 id="Merge-only"><a href="#Merge-only" class="headerlink" title="Merge-only"></a>Merge-only</h2><p>现在,我们只考虑 merge,不考虑 pop 操作。假设 merge 了 $m$ 次,push 了 $k$ 次,最后得到的堆的节点数为 $n$ 。那么,总操作的时间复杂度取决于 merge_tree 进行了多少次 (因此 merge_heap 和 push 的实现本质都是借助 merge_tree) 。因此,我们依然只需分析进位了多少次,假设 merge 的其他的二项堆的数位和 (前面说过了,默认二进制下的) 分别为 $s_1,s_2\dots s_m$ 的,堆的大小为 $x_1,x_2\dots x_m$,设初始节点数 $n_0$ 数位和为 $A$,最终节点数 $n$ 的数位和为 $B$ 。由此可知,进位数量最多为</p><script type="math/tex; mode=display">(\sum_{i = 1}^{m}{s_i}) + k + A - B。</script><p>注意到 $s_i \le \log_2(x_i + 1)$,因此有</p><script type="math/tex; mode=display">\sum_{i = 1}^{m}{s_i} \le \log_2(\prod_{i = 1}^m{(x_i + 1)}) \le \log_2 \{ {(\frac{\sum_{i=1}^{m}(x_i + 1)}{m}) ^ m} \} \le m\log_2(1 + \frac n m)</script><p>然后,发现这么搞做不出来…</p><blockquote><p>你是fvv —— DarkSharpness</p></blockquote><p>好吧,其实再认真看这一条:</p><script type="math/tex; mode=display">(\sum_{i = 1}^{m}{s_i}) + k + A - B。</script><p>以及,$s_i \le \log_2(x_i + 1)$ …… 等一下! 每一次两个节点数为 $n_1$ 和 $n_2$ 的二项堆的合并操作要求地最坏复杂度为 $O(\log (n_1 + n_2)) $ 。 因此对于每个 $s_i$ 次 merge_tree 的操作,我们可以将其直接分摊到 merge_heap 操作上。此时,不难发现,第 i 次合并的时间复杂度为 $\log_2(x_i + 1) \times O(1) \le O(\log (n_1 + n_2))$ ,没有破坏 merge 的性质,而分给 k 次 push 操作的总复杂度最坏为 $\log n_0 + k$,其中 $n_0$ 为初始节点数。因此,类似之前 push 的分析,我们可以得到 : push 操作依然是均摊 $O(1)$ 的时间复杂度。</p><blockquote><p>哪有什么岁月静好,不过是另外还有一个人替你负重前行。</p></blockquote><h2 id="Pop-Merge"><a href="#Pop-Merge" class="headerlink" title="Pop + Merge"></a>Pop + Merge</h2><p>pop 和 merge 操作结合起来其实也差不多,借用前面单独分析情况的思想即可。我们只需要考虑依然只要考虑二进制下数位和的变化即可。假设连续 k 次插入操作,穿插了一些删除操作,和合并操作。假设删除前的数字为 $y_i$,那么删除后,数位最多减少 $\log_2 y_i$ 。而假设合并的数为 $s_i$,那么最多进位 $\log_2 s_i$ 次。类似地,设初始节点数数位和为 $A$,最终节点数的数位和为 $B$。那么最终进位次数不会超过</p><script type="math/tex; mode=display">\sum_i{\log_2 y_i}+ \sum_i{s_i} + k + A - B</script><p>我们可以把每一项 $\log_2{y_i}$ 平摊到每次的 pop,而每一项 $s_i$ 平摊到 merge 操作上。借用前面的 pop-only 和 merge-only 的分析,我们不难得出,这不会破坏两者最坏 $O(\log n)$ 的性质。因此,此时,假设初始节点数为 n ,那么插入 k 次后,push 分到的最坏总复杂度为 $O(k + \log n)$ 。因此,借用前面的 push 的证明,我们可以得知,该情况下, push 操作的均摊复杂度为 $O(1)$ 。</p><blockquote><p>哪有什么岁月静好,不过是另外有一群人替你负重前行。</p></blockquote><h1 id="ENDING-总结"><a href="#ENDING-总结" class="headerlink" title="ENDING + 总结"></a>ENDING + 总结</h1><p>总结下来,无论怎样的操作顺序,push 的均摊复杂度为 $O(1)$ ,同时依然保证 pop 、decrease_key 和 merge_heap 是 $O(\log n)$ 的最坏复杂度。而拷贝构造、移动赋值,移动构造则是线性的最坏复杂度,移动构造是常数的复杂度。</p><p>文章水完了,不值得一看。认真分析还得用势能分析法,将二进制数位和作为势能即可。</p><p>感谢您浪费了 10 多分钟,看这篇乐色、没用势能分析法分析的文章。</p>]]></content>
<summary type="html">学校的作业罢了,没啥好看的。</summary>
<category term="算法" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="堆" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/%E5%A0%86/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="堆" scheme="http://darksharpness.github.io/tags/%E5%A0%86/"/>
</entry>
<entry>
<title>红黑树的一些实现 & 剖析</title>
<link href="http://darksharpness.github.io/RBT1/"/>
<id>http://darksharpness.github.io/RBT1/</id>
<published>2023-03-14T08:42:16.000Z</published>
<updated>2023-03-17T04:39:50.000Z</updated>
<content type="html"><![CDATA[<p>红黑树是一种非常高效的数据结构。其本质是一颗平衡的二叉搜索树,可以在 $O(logn)$ 的时间内完成一次插入或者删除操作,支持在最坏 $O(logn)$ 的时间内树上进行二分查找的操作。在各种编程语言中,往往也会用红黑树算法来实现最基础的 map 类(或者其他类似的名字,例如 python 字典类),可以通过一个 key 来查询对应的 value 。</p><p>本文不会过分地讨论红黑树平衡相关的算法,而是会具体讨论下红黑树实现的底层架构,算法问题请左转百度自行学习。本文希望能够从一些最简单的想法出发,引出一个复杂而高效的红黑树实现。</p><h1 id="前置任务"><a href="#前置任务" class="headerlink" title="前置任务"></a>前置任务</h1><p><strong>在开始阅读文章前,请牢记以下这些核心问题与要求!</strong></p><p><strong>在开始阅读文章前,请牢记以下这些核心问题与要求!</strong></p><p><strong>在开始阅读文章前,请牢记以下这些核心问题与要求:</strong></p><h2 id="Questions"><a href="#Questions" class="headerlink" title="Questions:"></a>Questions:</h2><ul><li>如何简化繁多的边界判定以及分支 ?</li><li>如何减少不必要的重复代码 ?</li><li>如何尽可能地减小空间占用 ?</li><li>如何避免模板导致的代码膨胀 ?</li></ul><h2 id="Requirements"><a href="#Requirements" class="headerlink" title="Requirements:"></a>Requirements:</h2><ul><li>最坏 $O(n)$ 的遍历树(用迭代器),复制另外一颗树</li><li>最坏 $O(logn)$ 的插入、删除、二分查找操作等等</li><li>均摊 $O(1)$ 的迭代器自迭代操作(++ 和 — 双向迭代)</li><li>最坏 $O(1)$ 取得 begin() 和 end() 迭代器、迭代器解引用</li></ul><h1 id="回顾"><a href="#回顾" class="headerlink" title="回顾"></a>回顾</h1><p>先简单回顾下二叉搜索树。</p><h2 id="节点"><a href="#节点" class="headerlink" title="节点"></a>节点</h2><p>一颗二叉树的节点会保留一些必要信息。一般来说,一个节点必须要有指向左儿子和右儿子的两个指针,还有节点保存的数据,大致如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> {</span><br><span class="line"> Node *left;</span><br><span class="line"> Node *right;</span><br><span class="line"> value_type data;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>对于红黑树,其特别地还需要保留一个节点的颜色(Color)。不难发现,保存红黑树的节点只需要 1bit 即可,但是如果用一个 bool 变量来保存,那么由于 C++ 的对齐,其将会被拓展到 8bit 甚至更多 (一般来说至少会被对齐到 4Byte 或 8Byte)。因此,存在一种节约内存的优化,即把颜色嵌入某个指针的最低或者最高位 (事实上,在 64 位机器上,指针有很多bit 都是无效信息,因为指针只需 40bit 就可以唯一标号一个 1T 内存中每一个位置,一般多余的 bit 内存管理器会用来保存其他的信息,例如内存块的大小) ,当然这涉及程序甚至是系统底层 allocator 的具体实现,一般只有非常底层、贴近操作系统 (例如Linux内核) 的代码中才会考虑。</p><p>同时,若要实现迭代器遍历的功能,则必须额外存一个指向父亲节点的指针 parent ,原因也很显然,这里举一个最简单的例子(不是说只有这个情况) : 如果当前迭代器指向叶子节点 (即当前节点左右儿子指向空) ,那么向前或向后迭代该迭代器的时候,其必须要往父节点方向走,这就需要父亲指针 parent 的存在。(你可能想问为啥迭代器不能单独存一些额外信息来避免存 parent 指针,但是容易证明这需要额外保存最长 $O(logn)$ 级别的 parent 链,这样的开销对一个本应 simple 的迭代器是不可接受的)</p><p>下面是一个演示实现代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 颜色 变量,只需一个 bit 即可 */</span> </span><br><span class="line"><span class="keyword">enum class</span> <span class="title class_">Color</span> : <span class="type">bool</span> { BLACK,RED };</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 节点主体 */</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> {</span><br><span class="line"> Node *parent;</span><br><span class="line"> Color color;</span><br><span class="line"> Node *left;</span><br><span class="line"> Node *right;</span><br><span class="line"> value_type data;</span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="搜索树"><a href="#搜索树" class="headerlink" title="搜索树"></a>搜索树</h2><p>对于一颗搜索树,其显然需要保留一个根节点,或者说是指向根节点的指针。不仅如此,其也需要提供一个 allocator 来管理节点占用空间的申请与释放,还需要一个 Compare 类来负责比较两个 key-value pair 的 key (因为搜索树内部一般是按照 key 的大小顺序构建的)。当然,如果你的容器需要记录节点总数,那么还需要额外存一个 count 变量。</p><p>*题外话: 如果你还不知道如何啥是 allocator,说明你可能没怎么写过工程代码,但是别急,你可以理解为用来申请/释放内存空间的一个类,提供类似 new/delete,malloc/free 的功能。</p><p>因此,一个典型的基于搜索树类可以大致写成如下的形式:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">tree</span> {</span><br><span class="line"> allocator Alloc;</span><br><span class="line"> Compare comp;</span><br><span class="line"> Node *root;</span><br><span class="line"> <span class="type">size_t</span> count;</span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="亿朵乌云"><a href="#亿朵乌云" class="headerlink" title="亿朵乌云"></a>亿朵乌云</h2><blockquote><p>动力理论肯定了热和光是运动的两种方式,现在,它的美丽而晴朗的天空却被两朵乌云笼罩了。</p><p>现在,红黑树那看似美丽而晴朗的应用前景,也被亿朵乌云所笼罩。</p></blockquote><p>现在我们已经基本定义了红黑树的最核心的两个类 (好吧,应该是三个,算上 enum class Color 的话)。然而,问题真的解决了吗 ? 还有些许边界细节没有讨论清楚吧 ?</p><p>以下是一些是实现红黑树时常见的问题 (至少 DarkSharpness遇到了部分) :</p><ol><li>当树为空的时候,root 指向什么? 空?</li><li>allocator 和 Compare 往往是空类型,但是由于 C++ 要求至少会占用 1 Byte,对齐后可能就是 8 Byte,空间浪费如何解决?</li><li>iterator 类里面存什么? 一个指针? 还是两个?</li><li>如何保证获得 begin() 和 end() 都是最坏 O(1) 的?</li><li>end() 迭代器理论上指向的不是这个 map 中的元素。此时如何保证 —end() 能回到 map 中?</li><li>红黑树主体如此繁杂,如何尽可能避免模板类带来的代码膨胀? <a href="https://blog.csdn.net/zhizhengguan/article/details/113384008">(see this)</a></li><li>如何尽量复用代码,让程序主体看起来尽可能的简洁?</li></ol><p><strong>我们记作 7 朵乌云。</strong></p><p>事实上,仔细思考,这里面还是有很多细节的。下面将从具体实现入手,加以分析。</p><h1 id="深入"><a href="#深入" class="headerlink" title="深入"></a>深入</h1><p>为了尽可能地优化,我们需要结合一些 C++ 和 红黑树 的 Feature 进行针对性的优化。</p><h2 id="空基类优化"><a href="#空基类优化" class="headerlink" title="空基类优化"></a>空基类优化</h2><p><strong>针对前面的第 2 朵乌云</strong>,我们可以利用C++ 中的<strong>空基类优化</strong> (<a href="https://en.cppreference.com/w/cpp/language/ebo">Empty base optimization</a>)。简单来说,虽然 C++ 要求任何类型的 sizeof 至少为 1 来保证两个不同的对象有不同的地址 (想想sizeof()是 0 的时候,数组里面不同的对象的地址情况) 。但是对于空基类 (即没有虚函数和非 static 的变量),如果衍生类除去基类的部分 sizeof() 不是 0,且满足一些特殊的条件 (详见前面的link),那么可以把空基类的地址设置的和衍生类第一个 sizeof 不是 0 的变量一样,这样就可以避免空类型占用空间。本例中,由于要记录树节点数量 count,所以将其 allocator 和 Compare 压缩到 count 上。</p><p>参考实现如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Utilizing EBO</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">implement</span> : allocator,Compare {</span><br><span class="line"> <span class="type">size_t</span> count;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果 allocator 和 Compare 为空 , 正确</span></span><br><span class="line"><span class="built_in">static_assert</span>(<span class="built_in">sizeof</span>(<span class="type">size_t</span>) == <span class="built_in">sizeof</span>(implement));</span><br></pre></td></tr></table></figure><p><strong>由此,我们借助了 EBO 空类型优化,解决了乌云 2。</strong></p><h2 id="节点和树的关系"><a href="#节点和树的关系" class="headerlink" title="节点和树的关系"></a>节点和树的关系</h2><p>节点 Node 是树 tree 的重要组成部分,而 tree 又是必须依赖于模板里面的 Compare 和 allocator的,但是,这真的意味着两者不可分离吗? 难道所有的原子操作都是依赖模板参数的? 模板代码膨胀 <del>dssq</del> 不可避免了?</p><p>最简单的,我们粗糙地考虑下红黑树的一次成功的插入 insert 操作: 首先通过树,定位到对应的插入的位置,构造并插入一个 Node,然后开始调整树的红黑关系。</p><p><strong>现在给你半分钟。仔细思考以上的步骤中,哪些是必须依赖模板类 tree 的,哪些是可以独立于 tree 而只依赖于 Node 而进行的。在这里,我们假设 tree 和 Node 是分离的。</strong> 你可以带着问题先看下去。</p><p><strong>我们考虑前面的第 1 朵乌云</strong>: root 存什么。事实上,root 是一个很特殊的指针,其指向的是红黑树的根节点。而当 root 所指向的节点改变,例如在 红黑树旋转 rotate 操作后,root 的值也应当随之改变。然而 rotate 操作修改的是相关 Node 的指针,而不能修改 root 这一个属于 tree 的指针。这便会导致如果 rotate 旋转后改变了根节点,那么你将必须要回到 tree 去修改 root,或者在 rotate 参数传入 root 指针引用。这样的代码显然是不好的,将导致 rotate 过多依赖于 tree 。事实上,如果把 root 指针嵌入某个 Node,即 root 其实是某个 Node 的指针,那么我们在 rotate 中就可以直接通过修改 Node 的指针来修改 root,从而达到脱离 tree 只借助 Node 来 rotate。</p><p><strong>一分钟差不多也到了吧(笑)。</strong>从上面这个例子,我们不难看出,在把 root 嵌入 Node 以后,我们可以把 rotate 操作直接独立于 tree,只借助 Node 之间的修改操作 (一般是通过Node *间接修改),来实现。再回到前面那个问题,在 insert 中,定位到插入位置依赖于比较函数 Compare,而构造并插入 Node 依赖于 allocator,但是调整树的红黑关系,它只需要知道 Node 的颜色,修改 Node 的属性即可了。因此,我们可以认为调整 Node 颜色这一步是独立于 tree 进行的。</p><p>但是,别忘记了,我们的 Node 目前还是依赖于树的 value_type 呢,两者独立只是我们的假设,重新看看节点最原始的定义:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 节点主体 */</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> {</span><br><span class="line"> Node *parent;</span><br><span class="line"> Color color;</span><br><span class="line"> Node *left;</span><br><span class="line"> Node *right;</span><br><span class="line"> value_type data;</span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这就很 annoying 了。但是,仔细思索前面提及的 Node 的操作 : rotate 和 insert 后调整颜色。两者似乎…似乎有什么共性? <strong>两者都不依赖于 value_type !</strong> 是的,当你完成插入以后,他已经是一颗标准的二叉树了,无论怎么 rotate ,怎么调整颜色,都不会影响到 value_type。不仅如此,value_type 也不会影响到后续核心的 insert 后调整颜色。说到这,你是不是有一种感觉,感觉 Node 是一个不够原子不够本质的东西? 好像还能再提取些什么出来,其完全不依赖于 tree 所提供的模板。由此,DarkSharpness 想出了两种不同的思路:</p><ul><li>将 value_type 换成一个 void * 指针,需要时指针类型强转。</li><li>将 Node 拆成一个不含 value_type 的基类 Node_base 和继承了 Node_base、依赖于模板 的 Node 类。进行不依赖 tree 操作时,Node 指针安全退化为基类 Node_base 指针。</li></ul><p>显然的,后者会略好于前者。首先,前者会多存一个指针的大小,这个开销时完全可以避免的。同时,后者用到了隐性类型转化 (派生类退化为基类) 会更加安全。最后,前者用指针简介访问存储的数据,其实会带来一定的性能开销 (访问数据相当于再经过了一层指针,而指针寻址不是很高效)。</p><p>所以,在综合多种考虑后,DarkSharpness 决定采用第二种实现: 将 Node 拆成一个不含 value_type 的基类 Node_base 和继承了 Node_base、依赖于模板 的 Node 类。进行不依赖 tree 操作时,Node 指针安全退化为基类 Node_base 指针。如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 基类 */</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node_base</span> {</span><br><span class="line"> Node_base *parent;</span><br><span class="line"> Color color;</span><br><span class="line"> Node_base *left;</span><br><span class="line"> Node_base *right;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 派生类 */</span> </span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> : Node_base {</span><br><span class="line"> T data;</span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而 rotate 等部分红黑树操作也就可以独立于模板,从而避免代码膨胀了。大致如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 将当前节点旋转到父节点位置</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">rotate</span><span class="params">(Node_base *ptr)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 插入当前节点后,完成红黑树的修复(调整颜色,改变相对位置rotate)</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">insert_fix</span><span class="params">(Node_base *ptr)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>总结一下,通过综合考虑红黑树的 tree 和 Node 的关系,我们把之前朴素想法的 Node 类拆成了更加本质的 Node_base 和模板派生类 Node ,从而将部分的函数直接独立于模板类 tree 实现。事实上,这部分函数占据的是红黑树的主体部分,这样的实现方式非常有助于减少代码膨胀。</p><p><strong>由此,乌云 6(其实也就是核心问题中的代码膨胀问题) 得以部分被解决。</strong></p><h2 id="从迭代器到根结点、root-指针架构"><a href="#从迭代器到根结点、root-指针架构" class="headerlink" title="从迭代器到根结点、root 指针架构"></a>从迭代器到根结点、root 指针架构</h2><p>(大段文字警告!⚠ 请耐心阅读,谢谢配合!)</p><p>在前面节点和树的关系的讨论中,我们首先指出了 root 指针(注意,不是 root 节点) 应该嵌入一个 Node ,这样可以便于节点和树进一步地分离,例如在 rotate 操作中修改 root 的话只需修改 root 所在 Node 的一个指针即可,不需要知道模板类 tree 内的信息。随后,我们又为了分离 tree 和 Node ,把 Node 拆分为了 Node_base 和 派生模板类 Node。因此,我们可以进一步地细化,指出 root 应该嵌入一个 Node_base 中,作为其一个指针。我们把这个特殊的 Node_base 称作 Header。</p><p>需要特别注意的是,root Node 和 root 指针不是一个东西,root Node 是一个具体的 Node,root 指针是一个指针变量,其指向 root Node,应该是 Header 的一个成员变量。</p><p><strong>但是</strong>,你真的认为这样就结束了吗 ? 如何保证根节点能正确地修改(例如 rotate 操作,根结点不用特判吗 ? 怎么判断是根结点呢 ? ) ? root 指针具体如何嵌入 Header ? 是作为 Header 的 left 指针 ? right 指针 ? 难道是 parent 指针 ? 剩下两个不记录 root 的指针记录什么呢 ? 这个 Node_base 的 Color 有用吗? root Node 的 parent 是什么呢 ? 这些问题似乎都没有一个明确的答案。</p><h3 id="iterator-存在的问题"><a href="#iterator-存在的问题" class="headerlink" title="iterator 存在的问题"></a>iterator 存在的问题</h3><p>你先别急。我们先来简单分析下 iterator 的迭代过程。当一个 iterator 要自增,其操作大致如下:</p><ul><li>有右儿子<ul><li>后继节点是右子树的最左边的叶子节点。</li></ul></li><li>没右儿子<ul><li>往上走,直到当前节点不是的父节点右儿子,此时父节点为后继节点。</li><li>上述操作走到了根结点,说明当前节点就是书上最大节点。</li></ul></li></ul><p>这没啥好分析的,非常自然,容易由二叉树的定义导出。代码也很简单,参考如下。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 返回后继节点,相当于 ++ */</span> </span><br><span class="line"><span class="function">Node_base *<span class="title">next_Node</span><span class="params">(Node_base *current)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(current->right) {</span><br><span class="line"> current = current->right;</span><br><span class="line"> <span class="keyword">while</span>(current->left)</span><br><span class="line"> current = current->left;</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">/* is_root 是一个特殊的在类外判断是否为根的函数 */</span></span><br><span class="line"> <span class="keyword">while</span>(!<span class="built_in">is_root</span>(current)) {</span><br><span class="line"> Node_base *parent = current->parent;</span><br><span class="line"> <span class="keyword">if</span>(parent->left == current) <span class="keyword">return</span> parent;</span><br><span class="line"> current = parent;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>; <span class="comment">// 当前节点无后继</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">/* 返回前驱节点,相当于 --*/</span> </span><br><span class="line"><span class="function">Node_base *<span class="title">prev_Node</span><span class="params">(Node_base *current)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(current->left) {</span><br><span class="line"> current = current->left;</span><br><span class="line"> <span class="keyword">while</span>(current->right)</span><br><span class="line"> current = current->right;</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">/* is_root 是一个特殊的在类外判断是否为根的函数 */</span></span><br><span class="line"> <span class="keyword">while</span>(!<span class="built_in">is_root</span>(current)) {</span><br><span class="line"> Node_base *parent = current->parent;</span><br><span class="line"> <span class="keyword">if</span>(parent->right == current) <span class="keyword">return</span> parent;</span><br><span class="line"> current = parent;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>; <span class="comment">// 当前节点无前驱</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>先不管缺失的 is_root 函数,我们暂时只考虑迭代器 end() 自减的问题。—end() 可以使得迭代器指向 tree 中最大的元素,但是问题在于,end() 应该存储什么信息 ? 如果 end() 是空指针的话,那么仅靠它没有任何方法走向 tree 中最大的元素。</p><p>你可能会说,“那么我们不要求迭代器可以反向迭代不就可以了吗”。这不是一个好的办法。事实上,Node_base 的结构天生的就保证了双向迭代的可行性,如果不实现反向迭代,其实某种程度上的功能浪费,没能 exploit potential to its full. Anyway ,笔者在文末也会单独讨论这种情况的特殊实现,但是现在我们暂时认为这是一个不好的解决方案。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>针对 end() 的问题,一种简单的解决方案是在迭代器中多存一个指向 tree 的指针,end() 存空指针,特判 —end() 即可。这样也的确是一种可行的方案,但是毕竟多存了一个不必要的指针,还是有一定空间上的浪费,而且除此之外该指向 tree 的指针毫无用途,只有一个地方有用,真的浪费。</p><p>还有一种思路,是让 end() 指向树中的某一个特殊节点,满足其正常 — 操作后指向最大的节点。然而,容易证明,在树上没有这样的节点,而且会导致迭代器相等的判定出错。</p><p>我们或许可以在最大节点下面再挂一个节点(作为右儿子)。<strong>这势必会带来更多的不必要的特判,破坏了代码的简洁性,更重要的是容易导致 tree 和 Node 的分离失效,是应该避免的,请务必记住这点,感兴趣的话自己写写看就知道了</strong>。</p><p>但是,等等! 我们只说了树上的节点,还没说不是树上的节点。我们还有一个 Header! 这便是突破口 : <strong>我们可以让 end() 指向 Header</strong> ! 但是,我们依然要保证以下两条特殊性质:</p><ol><li>指向最大节点的迭代器 ++ 会到达 end()</li><li>end() 迭代器 — 能回到最大节点。这是两个特殊的性质。</li></ol><p>后面将具体分析。</p><h3 id="特殊性质-1"><a href="#特殊性质-1" class="headerlink" title="特殊性质 1"></a>特殊性质 1</h3><p>让我们回到 prev_Node 和 next_Node 函数,仔细再分析一下。对于第一个性质,最大节点的迭代器 ++ 即 next_Node 会到达 end(),针对 next_Node 的两个 if 分支,一个解决方案是在最大节点下面挂一个节点 (右儿子) ,但是前面已经说过了,会带来大量不必要的特判,非常麻烦。另外一个解决方案,就只能是最大节点在祖先链条上。而显然的,对于最大值所在节点,++ 的时候能走到根节点并且停下来。因此,一个很自然的想法出现了 : 我们让根节点的 parent 指针指向 Header,这样,我们 ++ 的时候就可以通过不断地往父亲节点走,最终从 root 到达了 Header,即所谓的 end() 指针。这看起来真的太棒了! 它几乎不会引入什么额外的分支。</p><p>不过需要注意的是,此时对于最大值迭代器 ++ 的情况,仅仅让根节点的 parent 指针指向 Header 需要一个 is_root 函数。但是当根结点的 parent 指针指向的是 Header 而不是空的时候,我们暂时还不能很高效地、不依赖于 tree 地判断当前节点是不是根。因此,一种可行的架构是,让 Header 的 left 指针指向根结点。此时最大值迭代器 ++ 自然会停在 Header 即 end() ,因此不用再特判。同时,另外一种可行的架构是,让 Header 的 right 指针指向根结点。此时,最大值迭代器 ++ 会一路走到 Header,而我们只需要一个能在 tree 外判断是不是 Header 的函数即可,例如将 Header 的 parent 指针置空,通过检测 该特定指针来判断是不是 Header。当然,还有一种可行的架构是 Header 的 parent 指向根结点,这样子,根结点和 Header 都具有性质: 当前节点的 parent 的 parent 是自己,从而可以在根结点特判解决。</p><p>总结一下,我们暂时做的是:</p><ul><li>把根节点的 parent 指针指向 Header。</li></ul><p>我们可选的方案是:</p><ol><li>去除 is_root 特判,直接将 Header 的 left 作为 root 指针(指向根结点)。</li><li>将 Header 的 right 作为 root 指针(指向根结点),将 Header 的 parent 指针置空用于判别 (额外占用一个指针)。</li><li>将 Header 的 parent 作为 root 指针(指向根结点),特判根结点和 Header 具有 parent 的 parent 指向自己的性质。</li></ol><p>三者开销几乎一致,暂时看来 1 略优于 3 略优于 2。</p><h3 id="特殊性质-2"><a href="#特殊性质-2" class="headerlink" title="特殊性质 2"></a>特殊性质 2</h3><p>OK,问题已经解决了一半,剩下另一个性质是: 保证 —end() 可以到达最大值所在节点。而通过 — 即 prev_Node ,针对 prev_Node 的两个 if 分支,一个解决方案是将 Header 的 parent 指向最大节点,但是缺点也很明显 : 此时最大节点的右儿必须指向 parent (为了 next_Node 中的特判),这已经分析过了,势必会带来大量不必要的特判。剩下的解决方案是将 Header 的 left 指针指向根结点到最大节点路上的任意一个节点。一般来说,正常人只会考虑其中两种情况 : 指向根结点和指向最大值节点。</p><p>但是,结合前面特殊性质 1 里面提到的可选方案,你会发现将 Header 的 left 指针指向 root 节点几乎是完美的。这相当于 Header 存储了一个永远大于最大值的值,因此可以保证 —end() 可以到达最大值节点,反过来也可以通过 ++ 到达。这样的实现看起来就非常优雅、简洁,唯一的问题是Header 节点在树中的关系不是太和谐,他不是一个对称的存在而是一个永远最大的存在。</p><p>当然,我们也不能忽略其他几种相对没那么完美的实现。对于特殊性质 2 里面的可选方案 2 似乎的确会带来很多麻烦事情,但是,可选方案 3 并不是一个无用的存在。在方案 3 中,如果将 Header 的 left 指针指向最大值节点,同样可以 —end() 到达最大值节点,而且可以把这一次操作的复杂度降低到 $O(1)$ 。不仅如此,如果出于对称性,将 Header 的 right 指针指向最小值节点,你会发现整颗树连起来,我们可以让最大值迭代器 ++ 回到 Header (end()),再 ++end(),可以直接走到最小值节点。由此,一颗树被连了起来,无论如何 ++ — ,迭代器都不会走出 树的范围(包括 Header)。不仅如此,此时树的 begin() 迭代器也可以通过访问 Header 的 right 指针在 $O(1)$ 的时间内完成,还顺便解决了第 4 朵乌云。而且此时 Header 节点是完全对称的,这样的实现比起前面看似“完美”的实现,要更加的对称、优雅,也是笔者采用的实现方式。</p><p>不过,这也不是唱衰前面那种“完美”实现,其也可以通过将 Header 的 right 指向最大值节点,以及 Header 的 parent 指向自己 (仔细想想是为什么,如何实现,留作思考题,欢迎在评论区留言) ,来实现和方案 3 一样的功能 : 永不越界的迭代器、常数时间的 begin() 和 end()。只不过,比起方案 3 ,方案 1 的不对称性令人略有不爽。事实上,方案 1 实现的迭代器不需要根结点特判 (is_root 函数),可能实际时间效率还略高于方案 3 。</p><p>同时,其实仔细再想想,方案 2 也不是不行,只需将 方案 1 镜像以后,即 Header 的 left 指针指向最大值节点,right 指针指向最根结点,parent 指针指向自己 (同样的,自行思考为什么指向自己不会出 bug)。不过问题在于,begin() 函数指向最小值节点未维护,因此其是 $O(\log n)$ 的时间复杂度,其实并不优。</p><p>仔细考虑后,你才会发现,其实可选的实现方案真的很多。</p><h3 id="小总结"><a href="#小总结" class="headerlink" title="小总结"></a>小总结</h3><p>综上,我们在思考 root 指针和根结点架构的时候,为了满足迭代器的一些性质,做出了针对性的一些优化。最终架构如下:</p><ul><li>root 指针嵌入 Header 中</li><li>根结点的 parent 指针指向 Header</li></ul><p>Header 的可选方案只剩以下两种:</p><ol><li>Header 的 left 指针指向根节点,right 指针指向最小节点,parent 指针指向自己</li><li>Header 的 left 指针指向最大节点,right 指针指向最小节点,parent 指向根结点</li></ol><p>笔者使用方案 3,不过两者均可以满足:</p><ul><li>迭代器可双向加减,包括 end()。</li><li>迭代器 ++ — 不会越界,整棵树连起来。</li><li>begin() 和 end() 都是 $O(1)$ 时间</li><li>tree 和 Node 依然可以很好地分离。</li></ul><p>而利用了以上实现,我们也不难发现,此时迭代器只需存指向节点的指针即可,因为 ++ — 完全不会有越界问题,parent 指针的存在 以及 Header 和 root 的特殊架构保证了这一点。</p><p><strong>至此,乌云 1,3,4,5 彻底解决。</strong></p><h2 id="剩下的实现细节"><a href="#剩下的实现细节" class="headerlink" title="剩下的实现细节"></a>剩下的实现细节</h2><p>讲到这里其实基本没啥好讲的了,笔者具体来讲讲实现上的一些细节,顺便解决最后的第 7 朵乌云。</p><h3 id="从代码复用到-Node-架构"><a href="#从代码复用到-Node-架构" class="headerlink" title="从代码复用到 Node 架构"></a>从代码复用到 Node 架构</h3><p>看到这个副标题,你可能会困惑。 WTF ? Node(其实是 Node_base) 架构又要改了,那不是前面都白弄了 ? 你先别急。不是这样的。我们不会改变 Node_base 原有的架构 : 两个儿子指针,一个父亲指针,一个颜色。那还还能改什么呢 ? 继续看下去。</p><p>前面已经提到了 ++ 和 — 的底层函数 : next_Node 和 prev_Node 。你有没有想过,这两个函数是可以压缩成为一个的。的确,这两个操作本质上就是镜像操作。我们再考虑下正常平衡树的旋转操作 rotate 。一般来说,他会被分为两个函数 : zig 和 zag 。然而,<del>愚蠢的笔者分不清 zig zag 哪个是左旋,哪个是右旋</del>这么对称的两个函数分开实现,真的不会感到重复吗 ? </p><p>不难看出,红黑树上有许多的操作是镜像对称的,例如 next_Node (++) 和 prev_Node (—),还有 zig 和 zag。这些操作明显有点重复,如何把他压缩成为一个函数呢? 我们以 next_Node 和 prev_Node 为例分析。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 返回后继节点,相当于 ++ */</span> </span><br><span class="line"><span class="function">Node_base *<span class="title">next_Node</span><span class="params">(Node_base *current)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(current->right) {</span><br><span class="line"> current = current->right;</span><br><span class="line"> <span class="keyword">while</span>(current->left)</span><br><span class="line"> current = current->left;</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">/* is_root 此时视架构而变 */</span></span><br><span class="line"> <span class="keyword">while</span>(!<span class="built_in">is_root</span>(current)) {</span><br><span class="line"> Node_base *parent = current->parent;</span><br><span class="line"> <span class="keyword">if</span>(parent->left == current) <span class="keyword">return</span> parent;</span><br><span class="line"> current = parent;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> current; <span class="comment">// 返回参数视架构而定</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">/* 返回前驱节点,相当于 --*/</span> </span><br><span class="line"><span class="function">Node_base *<span class="title">prev_Node</span><span class="params">(Node_base *current)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(current->left) {</span><br><span class="line"> current = current->left;</span><br><span class="line"> <span class="keyword">while</span>(current->right)</span><br><span class="line"> current = current->right;</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">/* is_root 此时视架构而变 */</span></span><br><span class="line"> <span class="keyword">while</span>(!<span class="built_in">is_root</span>(current)) {</span><br><span class="line"> Node_base *parent = current->parent;</span><br><span class="line"> <span class="keyword">if</span>(parent->right == current) <span class="keyword">return</span> parent;</span><br><span class="line"> current = parent;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> current; <span class="comment">// 返回参数视架构而定</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>不难发现,两个函数只需将 left 和 right 简单替换即可,parent 指针部分不用改变。这时候,我们可以从 OIer 的代码中获得一定的启发。</p><p><img src="https://s2.loli.net/2023/03/17/6cmJS3Kaiu2FMxZ.png" alt="RBT2.png"></p><p>这是 OIwiki 上平衡树 Treap 的板子。注意,left 和 right 指针用了一个数组来代替。这样的设计真的是太妙了! 通过调整下标,我们就可以访问不同地儿子,这巧妙地规避了不对称性。</p><p>此时,++ 或 — 函数可以通过额外传递一个参数来表示,进而将 next 和 prev 压缩为一个函数,记作 advance ,实现如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Node_base</span> {</span><br><span class="line"> Node_base *parent;</span><br><span class="line"> Color color;</span><br><span class="line"> Node_base *son[<span class="number">2</span>];</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">/* dir = 0 走向前驱 || dir = 1 走向后继 */</span></span><br><span class="line"><span class="function">Node_base *<span class="title">advance</span><span class="params">(Node_base *current,<span class="type">bool</span> dir)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(current->son[dir]) {</span><br><span class="line"> current = current->son[dir];</span><br><span class="line"> <span class="keyword">while</span>(current->son[!dir]) <span class="comment">// 另外一个方向</span></span><br><span class="line"> current = current->right;</span><br><span class="line"> <span class="keyword">return</span> current;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> Node_base *parent = current->parent;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Header 方案 1 的写法 */</span> </span><br><span class="line"> <span class="keyword">if</span>(parent->son[dir] == current) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="keyword">return</span> parent;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Header 方案 3 的写法 */</span></span><br><span class="line"> <span class="keyword">if</span>(parent->parent != current && parent->son[dir] == current) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="keyword">return</span> parent;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>当然,由于这个函数本来也不是一个很大的函数,也不包含模板,因此,你也可以将 dir 参数设置为模板参数,通过额外生成一份模板(bool 也就两种)来加速代码,当然这都是小问题了。</p><p>不过,对于前面提到的 rotate 函数,其实可以简化到连额外地 bool 参数都不传递,因为 rotate 本质是将一个节点和其父节点交换,而我们借助 parent 指针就很容易知道是左旋还是右旋了。</p><h3 id="从代码复用到迭代器到-Header-架构"><a href="#从代码复用到迭代器到-Header-架构" class="headerlink" title="从代码复用到迭代器到 Header 架构"></a>从代码复用到迭代器到 Header 架构</h3><p><a href="/iterator/#迭代器的特殊实现"> 参考这个 </a> ,我们不难发现,我们只需为 iterator 提供一个 advance 模板函数即可。幸运的是,前面的 next_Node 和 prev_Node 的简化函数 advance 模板化以后,就是我们想要的函数。</p><p>由此,我们可以很轻松的借助那篇文章提供的思路,实现一个 map 的迭代器,这里暂时先不给出代码了。</p><p>但是,当我们要实现反向迭代器的时候,我们就不得不思考一下 : 对应的,我们有 $O(1)$ 的 rbegin() 和 rend() 函数吗。</p><p>对于方案 1,其没保存指向最大值的节点。因此,不可避免地,rbegin() 将会是 $O(\log n)$ 的复杂度。而对于方案 3 ,其由于极高的对称性,依然可以保证 $O(1)$ 的 rbegin() 函数。</p><p>因此,如果要实现反向迭代器,并且也保证 $O(1)$ 的 rbegin() 和 rend() 函数,那么留下来的唯一的选择就是 方案 3。具体如下:</p><ul><li>Header 的 left 指针指向最大节点,right 指针指向最小节点,parent 指向根结点</li></ul><h1 id="Ending-结束了"><a href="#Ending-结束了" class="headerlink" title="Ending 结束了?"></a>Ending 结束了?</h1><p>至此,核心问题和需求已经基本解决了。</p><p>但是似乎,还有啥没写完…? 我们为什么一定要反向迭代器 ? 为什么我们一定要保证可以反向迭代 ? 难道不能搞个单向的迭代器 ? 三指针的架构真的已经最优了吗 ?</p><p>暂时先更新到这里,有啥想问的评论区都可以问 qwq。</p>]]></content>
<summary type="html">红黑树的一种高效架构及实现。</summary>
<category term="算法" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="平衡树" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/%E5%B9%B3%E8%A1%A1%E6%A0%91/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="平衡树" scheme="http://darksharpness.github.io/tags/%E5%B9%B3%E8%A1%A1%E6%A0%91/"/>
<category term="STL" scheme="http://darksharpness.github.io/tags/STL/"/>
</entry>
<entry>
<title>关于 STL iterator 的那些事</title>
<link href="http://darksharpness.github.io/iterator/"/>
<id>http://darksharpness.github.io/iterator/</id>
<published>2023-03-08T10:11:37.000Z</published>
<updated>2023-03-17T03:15:20.000Z</updated>
<content type="html"><![CDATA[<p>希望您在阅读本文前对于迭代器、容器等概念已经略有了解,如果您不了解好像也没啥大关系。</p><h1 id="为什么要有迭代器"><a href="#为什么要有迭代器" class="headerlink" title="为什么要有迭代器"></a>为什么要有迭代器</h1><blockquote><p>读懂自己,迭代自己 —— <a href="https://zhuanlan.zhihu.com/p/82644069">yyu</a></p></blockquote><h2 id="迭代器的起源"><a href="#迭代器的起源" class="headerlink" title="迭代器的起源"></a>迭代器的起源</h2><p>笔者最初接触 iterator 是在学习 std::map 的时候。为了从小到大遍历访问 map 里面的元素,我们需要用到 iterator。</p><p>iterator,迭代器,顾名思义就是一个可以支持迭代的工具类,其最基础的功能就是通过自我迭代来遍历访问一个容器中的元素,即是对于 <strong>遍历访问容器的内容的操作</strong> 进行了封装。</p><p>这样的封装是非常有意义的,对于不同的容器,例如链表、数组、树,遍历访问的方式可能大不相同。如果没有迭代器,那么对于一些需要遍历容器的函数,例如 average() 获取容器中元素的平均值,我们需要针对每个容器数据存储的特点,设计其独有的 average() 函数。这样子不利于代码复用,而且更加容易在编写的过程中出错,甚至还容易过度暴露容器内部的细节,影响了封装性。</p><p>事实上,我们重新回到遍历访问容器中每一个元素这一个过程,其本质上可以拆成如下两个原子过程:</p><ol><li>对当前的元素(对象)执行操作</li><li>将操作的对象修改为下一个元素</li></ol><p>由此,我们可以对所有容器添加一些公有的接口,其会返回一个容器独特的迭代器,而这些迭代器都至少具有如下两个功能:</p><ol><li>访问当前位置</li><li>移动到下一个位置</li></ol><p>同时,遍历访问也是有终点的,所以我们还必须要能判断什么时候应该停止迭代,即何时到达了终点的位置,于是便有了迭代器的第三个功能: </p><ol><li>判断两个迭代器是不是同一位置。</li></ol><p>基于以上的这些原则,便有了 iterator 这个产物。以上三个功能,在 iterator 上分别对应的是</p><ol><li>类似指针,用 * 或 -> 访问</li><li>支持 ++ (有时包括 —) 运算符</li><li>可以对同类的 iterator 进行 == 或 != 运算符比较</li></ol><p>借助 iterator ,我们可以方便的遍历容器,且无需考虑内部的细节,也可以写出适用于各种有 iterator 的函数,而不必对每个容器重载一遍这个函数。</p><h2 id="线性容器的迭代器之思"><a href="#线性容器的迭代器之思" class="headerlink" title="线性容器的迭代器之思"></a>线性容器的迭代器之思</h2><p>如果 iterator 真的简单如此,那么为什么 vector,array 这类线性容器还要有 iterator 呢?用指针替代不就行了吗?指针的确能完美覆盖以上三个功能,而且性能不会也不可能坏于 iterator ,但 STL 依然为这些线性容器提供了 iterator ,这背后其实有更加深层的原因。</p><p>首先,笔者最早体会到的一点,也是比较次要的一点,就是指针本身不是一个类(class or struct),它是原生数据类型。而 C++ 原生数据类型的右值是不支持 ++ 和 — 运算符的。例如,假如一个函数 <code>int *begin()</code> 返回一个int 类型指针,那么你是不能直接进行 ++begin() 的操作,因为返回的begin() 是一个右值。而对于迭代器,只要重载了 ++ 运算符,即使是返回值右值 begin() 也可以直接进行 ++ 操作。</p><p>其次,在使用迭代器的时候,我们可能会想要知道迭代器的类型,例如对于随机访问迭代器,其向前迭代 n 次可以用 += n 来一步完成,而对于链表的迭代器,其向前迭代 n 次则只能一次次地迭代来实现。如果只用指针来作为迭代器,那么我们只能获得迭代器所指向的对象类型这一个信息,并不足以用来判断迭代器的类型。而线性容器 iterator 类则不一样。尽管为了性能,其本质上只存了一个指针,并且前进/后退等操作完全等价于一个指针,但其可以借助 traits 来判断类型,只会在编译期进行类型检测,不会有运行时开销,这也将会在后面讲到。</p><p>由此可见,iterator 并不仅仅是对于指针操作的一些包装,其还利用了 traits 和 模板技术,允许在编译期检测 iterator 的信息,便于用户能够针对性的写出不同的函数。</p><p>当然,指针的思想也深深地影响了迭代器的设计,目前STL的迭代器大部分在访问元素的操作上都极其类似指针,并且线性容器的迭代器几乎拥有指针所具有的所有性质(例如支持 * ->)。</p><h2 id="小总结"><a href="#小总结" class="headerlink" title="小总结"></a>小总结</h2><p>迭代器本质上是对于指针操作的一些包装,基于容器内部的细节实现,允许用户通过简单的迭代来遍历容器,正是 yyu 所云的 “读懂自己” 进而 “迭代自己”。同时,比起原生指针,尽管操作方式几乎一致(都可以迭代自增,判等以及访问元素),但是其借助了 traits 的力量可以在编译器表明自己的迭代器类型,不产生运行开销的同时,便于用户进一步地根据迭代器的类型来实现不同的函数,以不同的方式迭代,可谓是更深一层的 “读懂自己,迭代自己” 。</p><h1 id="迭代器进阶"><a href="#迭代器进阶" class="headerlink" title="迭代器进阶"></a>迭代器进阶</h1><h2 id="STL-迭代器基础"><a href="#STL-迭代器基础" class="headerlink" title="STL 迭代器基础"></a>STL 迭代器基础</h2><p>STL 为几乎所有常用的容器都提供了迭代器,例如 array,vector,deque,map,multimap,unordered_map,set等等。特别地(题外话,可跳),对于 priority_queue , queue 和 stack 这三个容器适配器,其并没有对应的迭代器,因为适配器本质是限制部分的功能,将一种容器转化为另外一种容器,其只保留了原容器部分必须的功能(例如 stack 只能在 top 处操作),而原容器并不保证允许迭代器访问。例如 std::stack 的底层容器只需要 size(),empty(),push_back(),pop_back(),back(),emplace_back() 这些接口即可,虽然标准库里面 std::stack 选择的底层容器是 std::deque 支持迭代器访问,但是仅包含这些接口的底层并不是都能支持迭代器。</p><p>对于这些迭代器,结合前面讲到的三条性质。</p><ol><li>访问当前位置</li><li>移动到下一个位置</li><li>判断两个迭代器是不是同一位置</li></ol><p>其必然可以支持以下的几种操作:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">test</span> { <span class="type">int</span> x; }</span><br><span class="line"></span><br><span class="line">iterator <test> i,j; <span class="comment">// 伪代码,仅表示 i,j 是某迭代器</span></span><br><span class="line"></span><br><span class="line">*i , i->x; <span class="comment">// 1. 访问当前位置元素,操作类似指针</span></span><br><span class="line">i++ , ++i; <span class="comment">// 2. 迭代自增,有的迭代器支持自减</span></span><br><span class="line">i == j, i != j; <span class="comment">// 3. 判断是否相等,有的支持比较( < 等)</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于常见 STL 容器,我们一般可以通过成员函数 begin() 或 end() 来获得其头部迭代器和尾部迭代器,分别指向 第一个元素的位置 和 最后一个元素后面的位置(这也是为什么不能访问 end() 指向的元素) 。其类型为 容器名字::iterator ,例如对于 <code>vector <int></code> ,其迭代器类型为 <code>std::vector <int>::iterator</code> 。对于 const 的容器,其 begin() 和 end() 返回的是const元素迭代器,即容器名字::const_iterator,其本身可以修改,迭代(不同于 const iterator),但是其指向的元素不能被修改(类似 const 数据类 的 非const 指针,例如 const int *)。</p><p>这时,肯定有小可爱要问了: 那么对于非 const 的容器,我们怎么获得 const_iterator 来安全访问呢?这非常简单,只需要用 cbegin() 和 cend() 成员函数来访问即可。</p><p>对于部分容器,其也支持反向迭代器,即可以从最后一个元素到第一个元素反向迭代。显然地,这样容器的正向迭代器必然支持反向迭代操作。</p><p>下面将再次结合三条性质进行分析</p><h3 id="1-访问当前位置元素的性质"><a href="#1-访问当前位置元素的性质" class="headerlink" title="1. 访问当前位置元素的性质"></a>1. 访问当前位置元素的性质</h3><p>对于序列容器,例如 vector,deque,list,一般来说其返回迭代器访问元素的方法完全等同于该类型的指针。下面以 list 为例。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">test</span> { <span class="type">int</span> x; }</span><br><span class="line">std::list <test> l = {test{<span class="number">1</span>},test{<span class="number">2</span>}};</span><br><span class="line">std::list <test> iterator i = l.<span class="built_in">begin</span>();</span><br><span class="line"><span class="comment">// 一般C++11 以后会用 auto</span></span><br><span class="line"><span class="keyword">auto</span> j = l.<span class="built_in">cbegin</span>(); <span class="comment">// const_iterator</span></span><br><span class="line"></span><br><span class="line">*i; <span class="comment">// 返回一个 test 类型可读写引用(test &)</span></span><br><span class="line">*j; <span class="comment">// 返回一个 test 类型只读引用(const test &)</span></span><br><span class="line">i->x; <span class="comment">// 相当于 (*i).x , 类似指针,类型为 int &</span></span><br><span class="line">j->x; <span class="comment">// 同上,但类型为 const int &</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于 set,multiset,unordered_set 和 unordered_multiset ,尽管其也有迭代器,但由于修改迭代器所指的元素可能会破坏内部的性质(红黑树 / hash),因此其 iterator 和 const_iterator 本质上都是 const_iterator 都是只读不可写的。</p><p>对于 map 和 unordered_map 等,其也类似 set ,对于键值 key 不能随便修改,但是允许修改 key 对应的 value。因此,其迭代器解引用的类型是 <code>std::pair <const key_type,value_type></code> ,对于 iterator 变量 i 我们可以通过 i->second 来修改当前位置 key-value pair 的 value 值。而 const_iterator 则什么都不能修改,是 read-only。</p><h3 id="2-移动到下一个位置的性质"><a href="#2-移动到下一个位置的性质" class="headerlink" title="2. 移动到下一个位置的性质"></a>2. 移动到下一个位置的性质</h3><p>该性质取决于迭代器的性质。</p><p>对于随机访问迭代器 (Random Access iterator),顾名思义可以随机访问,因此其不仅可以支持最基础的 ++ ,还允许进行 — 甚至是 + n 和 - n 以及迭代器之间相减这类操作,其本质原因是底层存储连续且有序 ,例如元素本身连续存储的 vector,array 等,或者指向元素的指针连续存储的 deque 等。</p><p>对于双向迭代器 (Bidirectional iterator),其显然可以反向迭代进行 — 操作,但是其略弱于随机访问迭代器,不能 + n 或者 - n。代表性的有 map,list 的迭代器</p><p>对于前向迭代器 (Forward iterator),其只有最基础的前向迭代 ++ 操作。代表性的有 forward_list,unordered_map 的迭代器。对于此类容器,其显然也不会有反向迭代器。</p><h3 id="3-判断所指是否相等的性质"><a href="#3-判断所指是否相等的性质" class="headerlink" title="3. 判断所指是否相等的性质"></a>3. 判断所指是否相等的性质</h3><p>其同样取决于迭代器的性质。</p><p>对于随机访问迭代器,其往往也会支持 < > 等比较操作,本质上是比较两个元素的相对下标之差,类似指针的比较。</p><p>而对于其他的迭代器,一般都不会支持除了 != 和 = 之外的比较运算符。</p><h2 id="iterator-traits"><a href="#iterator-traits" class="headerlink" title="iterator_traits"></a>iterator_traits</h2><p><del>咕咕咕…(待更新,欢迎催更)没人催也更新</del></p><p>std::iterator_traits 本质上是一种模板类,用来提供一个统一的接口,便于标准库基于迭代器的类型来实现不同的算法。而作为用户,只需提供一个含有一些特定的 typedef 的 iterator 类,或者是手动模板偏特化。</p><p>其定义大致如下:<br><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iter</span>></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">iterator_traits</span>;</span><br></pre></td></tr></table></figure></p><p>对于提供的 Iter 类,其必须要有以下五个成员类</p><ol><li>difference_type</li><li>value_type</li><li>pointer</li><li>reference</li><li>iterator_category</li></ol><p>其中,difference_type 是两个 iterator 做差后的类型(如果可以做差的话)。value_type 是解引用后得到的 value 类型。pointer 是指向 value 的指针类型。reference 是 value 类的引用类型。而 iterator_category 是 iterator 的类型。</p><p>这里有必要单独讲一讲 iterator_category 。其具体分为 input_iterator_tag , input_iterator_tag , forward_iterator_tag , bidirectional_iterator_tag , random_access_iterator_tag 。其含义比较直观,分别对应的是只读,只写,只能向前迭代,可以双向迭代,和可随机访问迭代。不难看出后面三个是一个比一个更强的,都至少需要前面一级作为基础,因此后三者应该呈继承关系。事实上,STL 里面也是这么实现的,下面是标准库的一些片段。(注释没改,来自 gcc 12.2.0 的原始片段)</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/// Marking input iterators.</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">input_iterator_tag</span> { };</span><br><span class="line"></span><br><span class="line"><span class="comment">/// Marking output iterators.</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">output_iterator_tag</span> { };</span><br><span class="line"></span><br><span class="line"><span class="comment">/// Forward iterators support a superset of input iterator operations.</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">forward_iterator_tag</span> : <span class="keyword">public</span> input_iterator_tag { };</span><br><span class="line"></span><br><span class="line"><span class="comment">/// Bidirectional iterators support a superset of forward iterator</span></span><br><span class="line"><span class="comment">/// operations.</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">bidirectional_iterator_tag</span> : <span class="keyword">public</span> forward_iterator_tag { };</span><br><span class="line"></span><br><span class="line"><span class="comment">/// Random-access iterators support a superset of bidirectional</span></span><br><span class="line"><span class="comment">/// iterator operations.</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">random_access_iterator_tag</span> : <span class="keyword">public</span> bidirectional_iterator_tag { };</span><br></pre></td></tr></table></figure><p>基于以上的知识,我们也可以为我们自己写的容器的 iterator 加上一些成员类型,从而使得其可以被 iterator_traits 识别。例如对于一个存储 int 的 array 类的 iterator ,其定义应该大致如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">iterator</span> {</span><br><span class="line"> <span class="keyword">public</span>: <span class="comment">// 注意,成员类需要 public! 否则无法识别</span></span><br><span class="line"> <span class="comment">// 为了C++ 11 之前的兼容性,可以用typedef 代替 using</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">using</span> difference_type = std::<span class="type">ptrdiff_t</span>;</span><br><span class="line"> <span class="keyword">using</span> value_type = <span class="type">int</span>;</span><br><span class="line"> <span class="keyword">using</span> pointer = <span class="type">int</span> *;</span><br><span class="line"> <span class="keyword">using</span> reference = <span class="type">int</span> &;</span><br><span class="line"> <span class="keyword">using</span> iterator_category = std::random_access_iterator_tag ;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line"> <span class="comment">// 后面是实现部分,省略</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">};</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看出,比起原本的实现,只需额外 typedef 或 using 声明几个模板类,即可满足迭代器被 iterator_traits 识别,从而可以用于标准库里面的某些函数。</p><p>这时候,聪明的您可能又要问了,那么原生指针不就没法支持了吗。您说得对,但是标准库对于原生的指针有模板特化,依然可以被 iterator_traits 所识别。</p><p>总结下来,iterator_traits 是对于原生指针和自定义的 iterator 的一层包装,用来提供一个统一的接口,便于 STL 实现针对性的算法。</p><p>而作为用户,也可以利用这一特性,实现针对性的算法。举例: 让迭代器前进 n 步。对于随机访问迭代器,只需 += 即可,但其他的迭代器需要一步一步的向前走。这时,可以借助 iterator_traits。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 如果是其他 input_iterator, 可以转换为基类 */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iter</span>></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">advance_n</span><span class="params">(Iter &i,<span class="type">size_t</span> __n,std::input_iterator_tag)</span> </span>{</span><br><span class="line"> <span class="keyword">while</span>(__n--) ++i;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 随机访问迭代器要特殊重载 */</span> </span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iter</span>></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">advance_n</span><span class="params">(Iter &i,<span class="type">size_t</span> __n,std::random_access_iterator_tag)</span> </span>{</span><br><span class="line"> i += __n;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iter</span>></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">advance_n</span><span class="params">(Iter &i,<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">advance_n</span>(i,__n,<span class="keyword">typename</span> iterator_traits <Iter>::<span class="built_in">iterator_category</span> ());</span><br><span class="line"> <span class="comment">// 空类型 iterator_category 无开销</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果您对这个感兴趣,可以去看看 cppreference 上关于 <a href="https://en.cppreference.com/w/cpp/iterator/iterator_traits">iterator_traits</a> 的解释。</p><h1 id="迭代器的特殊实现"><a href="#迭代器的特殊实现" class="headerlink" title="迭代器的特殊实现"></a>迭代器的特殊实现</h1><p>听了 DarkSharpness 讲了这么多,你难道不想要给自己写的容器实现一个迭代器吗。先别急,先看看我们要实现哪些:</p><ul><li>iterator</li><li>const_iterator</li><li>reverse_iterator (bonus)</li><li>const_reverse_iterator (bonus)</li></ul><p>先不考虑 bonus,你静下心来想想,便会发现 iterator 和 const_iterator 的行为几乎一模一样,唯一的区别在于,后者指向的对象是不可修改。仔细想想,如此重复的代码该如何提高复用 ? 你可以先思考,也可以直接看下去。</p><p>一开始,DarkSharpness 的想法是用 const_cast 等手段,先实现一个 iterator_base ,通过让两种 iterator 继承 iterator_base,来实现代码复用。这样子的弊端是,存在一个不太安全的 const_cast,以及还有额外实现 const_iterator 和 iterator 两个类型之间的比较函数,转换函数,这样子还是很不美观的。</p><p>这时候,神一般的 Wankupi 降临了。他使用了模板的思路,优化了 iterator_base ,通过传递模板参数 bool is_const 来判断是不是 const_iterator ,并且用到 C++ 11 以后的模板推导工具 std::conditional_t (不会自行百度qwq) 来实现当模板参数 is_const 为 false ,指针为 T * ,反之为 const T * ,其中 T 为解引用后的类型。</p><p>不过 Wankupi 的实现还是用来 iterator_base 和 iterator_common 两种类,这实在是太麻烦了。事实上,我们可以重新思考并抽象 iterator 的三个核心操作。</p><ul><li>访问当前位置</li><li>移动到下一个位置</li><li>判断是否在同一位置</li></ul><p>访问当前位置的操作,已经通过前面的 std::conditional_t 来解决了 const 与否的问题。</p><p>移动到下一个位置,其实本质上就是一个前进/后退函数。例如对于原生指针作为迭代器,其 advance 函数大致如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> pointer = <span class="type">int</span> *;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">advance</span><span class="params">(pointer &__p,<span class="type">bool</span> dir)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(dir) ++p;</span><br><span class="line"> <span class="keyword">else</span> --p;</span><br><span class="line"> <span class="comment">/* dir = 1 前进 || dir = 0 后退 */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然,也可以借助模板以及 C++ 11 以后的一些工具 (std::true_type 之类) ,将 dir 传入模板参数,让 dir 不同的时候进入不同的重载,进而生成更优的代码,效率上完全等价于真实指针,这里暂时不多提了,感兴趣可以看看 DarkSharpness 是如何实现随机访问迭代器的 advance 函数 <a href="https://github.com/DarkSharpness/DarkSharpness/blob/main/Template/Dark/Container/iterator.h">点我点我点我</a>。</p><p>判断是否在同一位置,其实本质上是判断指针指向的变量地址是不是同一个,此时不用成员函数实现反而会更优雅而简洁,只需对 iterator 类提供一个对外访问内部指针的接口即可。</p><p>事实上,一旦把前进过程用一个函数 advance 抽象化了以后,你会发现,四种 iterator (包括反向的两种) 的操作竟然是出奇的一致 :</p><ul><li>访问当前位置 (const / non-const)</li><li>移动到下一个位置 (reverse / normal)</li><li>判断是否在同一位置 (同类型比较)</li></ul><p>参考之前 const 模板化的过程,我们不难联想到 : 我们可不可以把 reverse 与否也扔到模板里面? 这个主意真的是太妙了。在把移动过程抽象为一个函数 advance 之后,这样的实现变得可行,其可以使得代码复用四次,复用率高了不是一点点。</p><p>参考代码如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Advance 函数需要自己实现,取决于容器内部结构 */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="type">bool</span> dir> <span class="comment">/* 0 后退 || 1 前进 */</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">advance</span><span class="params">(pointer &ptr)</span></span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* </span></span><br><span class="line"><span class="comment"> is_const 是否是 const 版本的迭代器</span></span><br><span class="line"><span class="comment"> dir = 0 反向迭代器 || 1 正向迭代器</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>,<span class="keyword">class</span> <span class="title class_">is_const</span>,<span class="type">bool</span> dir></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">iterator</span> {</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line"> <span class="keyword">using</span> U = std::<span class="type">conditional_t</span> <is_const,<span class="type">const</span> T,T>;</span><br><span class="line"> <span class="keyword">using</span> pointer = U *;</span><br><span class="line"> pointer ptr;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 这里以 ++ 和 -- 为例,其他都差不多 */</span></span><br><span class="line"></span><br><span class="line"> iterator &<span class="keyword">operator</span> ++ (<span class="type">void</span>) </span><br><span class="line"> { advance <dir> (ptr); <span class="keyword">return</span> *<span class="keyword">this</span>; }</span><br><span class="line"></span><br><span class="line"> iterator &<span class="keyword">operator</span> -- (<span class="type">void</span>) </span><br><span class="line"> { advance <!dir> (ptr); <span class="keyword">return</span> *<span class="keyword">this</span>; }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 同向迭代器,允许non-const -> const,赋值同理 */</span></span><br><span class="line"> <span class="built_in">iterator</span>(<span class="type">const</span> iterator <T,<span class="literal">false</span>,dir> &rhs) :</span><br><span class="line"> <span class="built_in">ptr</span>(rhs.ptr) {}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/* 用于 iterator 比较或其他操作 */</span></span><br><span class="line"> <span class="function">pointer <span class="title">base</span><span class="params">()</span> <span class="type">const</span> </span>{ <span class="keyword">return</span> ptr; }</span><br><span class="line"></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/* 同向迭代器可以比较,const 情况无所谓 */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">T</span>,<span class="type">bool</span> k1,<span class="type">bool</span> k2,<span class="type">bool</span> dir></span><br><span class="line"><span class="type">bool</span> <span class="keyword">operator</span> == (<span class="type">const</span> iterator <T,k1,dir> &lhs,</span><br><span class="line"> <span class="type">const</span> iterator <T,k2,dir> &rhs) {</span><br><span class="line"> <span class="keyword">return</span> lhs.<span class="built_in">base</span>() == rhs.<span class="built_in">base</span>();</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>就笔者个人而言,这样的写法真的是简洁而清晰,即使是对于原生指针的包装,在运行效率上也几乎完全等价于未包装的情况(模板),没有任何开销。事实上,这些代码编译器大概率会直接 inline 掉,此时就真的完美等价于原生指针了。</p><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1><p>感谢 <a href="https://apex.sjtu.edu.cn/members/yyu">yyu</a> 的<del>友情</del>客串。</p><p>感谢 <a href="https://www.wankupi.top">Wankupi</a> 提供的关于 iterator 和 const_iterator 模板化的思想。</p><p>感谢您认真看到了最后!</p>]]></content>
<summary type="html">关于 iterator 的一些想法</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="STL" scheme="http://darksharpness.github.io/categories/C/STL/"/>
<category term="STL" scheme="http://darksharpness.github.io/tags/STL/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>热学复习(上)</title>
<link href="http://darksharpness.github.io/thermodynamics1/"/>
<id>http://darksharpness.github.io/thermodynamics1/</id>
<published>2023-02-02T08:20:50.000Z</published>
<updated>2023-02-03T04:11:31.000Z</updated>
<content type="html"><![CDATA[<p>本文大概会整理一些常用的热力学知识和公式,逻辑<del>比较</del>非常混乱,主要是复习,还有发表一些错误的感想(段首会用*标识,完全不具有可信度)。笔者水平有限,欢迎在评论区<del>嘲讽他</del>一起讨论吧。</p><p><strong>注意,本页公式较多,渲染较慢,如有能力建议挂梯子加速。</strong></p><h1 id="热力学-统计"><a href="#热力学-统计" class="headerlink" title="热力学? 统计?"></a>热力学? 统计?</h1><p>热力学研究的对象是由大量微观的粒子分子组成的宏观物质系统。我们称与之作用的其他物体为外界。经验指出,一个孤立系统,无论其初态如何,其在足够长的事件后都会达到一种状态,其宏观性质在长时间内不发生改变,我们称之为热力学平衡态。有如下几个重要的概念或性质需要阐明:</p><ol><li>对于一个系统,其由最初的一个状态转变成平衡状态所经历的时间称为弛豫时间。</li><li>平衡态只是宏观性质稳定,但是微观下大量的微粒依然在不断地运动着,宏观量只是微观量平均的结果。</li><li>因为一般体系微粒数量巨大,尽管有大量偶然事件,其最后体现出来的结果几乎完全由统计规律决定。</li><li>因为上述性质,宏观量在平衡后也会有涨落,在极端条件下可以观察到,但是一般热力学里面不用考虑。</li></ol><h1 id="温度和热力学第零定律"><a href="#温度和热力学第零定律" class="headerlink" title="温度和热力学第零定律"></a>温度和热力学第零定律</h1><p>温度的概念最初来自生活,来自人体对于冷热的感知。温度,通俗意义上就是冷热程度。而测量温度,最朴素的方法就是接触法,温度计就是基于这样的原理设计。而这背后则暗含了一个前提,即用两个温度计测量同一个温度的物体,得到的温度是一样,这便引出了<strong>热力学第零定律 : 若两个热力学系统中的每一个都与第三个热力学系统处于热平衡(温度相同),则它们彼此也必定处于热平衡。</strong></p><p>*我们再仔细一想,这背后其实也暗含着一种逻辑:存在一种对于冷热程度的刻画标准,其不依赖一任何物质而独立存在。而在这个刻画标准下相同的任意两种物体或者是热力学体系,其处于热平衡状态而不存在热交换。当然,这样的标准可能有很多种。假设一个温度度量体系为 $T$ ,即满足 $T_1 = T _2$ 的2个物体或者热力学体系热平衡。那么考虑 $f(T)$ 这个衡量标准,$f$ 在定义域上单调。此时,只需 $f(T_1) = f(T_2)$ ,我们同样可以说两个物体热平衡。所以,其实温度的标准选取并没有一个绝对标准,热力学第0定律只是暗含了其存在性,而目前我们用的热力学温标 $T$ 则是其中一种而已,为对于理想气体可以表示为 </p><script type="math/tex; mode=display">\frac1{T} = (\frac {\partial{S}}{\partial{U}})_{V,N}</script><p>其中 $V,N$ 分别为体积和分子数,$S$ 是熵,$U$ 是内能,后面会讲到。而其单位 1K 则是由水三相点温度的 $1/273.16$ (倍数标准参考前面的方程) 定义。总之,对于温度这一相对比较抽象的概念,其严格定义与刻画比起可视的长度什么物理量,还是要复杂不少,我们目前的热力学温标还是很大程度上受到了理想气体温标 (其大小比较标准,倍数关系等) 和摄氏度温标 (单位温度 1K 的定义) 的影响。</p><h1 id="理想气体复习"><a href="#理想气体复习" class="headerlink" title="理想气体复习"></a>理想气体复习</h1><p>理想气体,是一种极其理想化的气体模型,是通过波意耳定律、盖吕萨克定律、查理定律这三条实验定律的结论,结合理想化模型得到。</p><p>我们假设气体的温度为 $t$ (不是热力学定义 $T$,这是常见的摄氏温标),体积为 $V$,压强为 $p$ ,那么上面三条定律可以分别表示为:</p><script type="math/tex; mode=display">pV = const</script><script type="math/tex; mode=display">V = V_0(1 + \alpha t)</script><script type="math/tex; mode=display">p = p_0(1 + \beta t)</script><p>特别的,在较低温度、较稀气体的情况下,通过实验得到</p><script type="math/tex; mode=display">\lim_{p\rightarrow0} \alpha = \lim_{p\rightarrow0} \beta = 1 / t_0</script><p>因此,我们重新定义 $ T = t + t_0 $ 为绝对温度(后来改称热力学温度),并且假设理想气体满足以上的两个条件,这样以上三个公式就可以总结为一个经典的公式</p><script type="math/tex; mode=display">\frac{pV}{T} = const</script><p>更进一步的,我们需要知道还有哪些气体状态参数会影响公式右边 const 。根据阿伏伽德罗定律,相同的温度和压强下,摩尔数相等的各种气体的体积相同。考虑以下情景:当我们有一团气体,往中间任何的位置插入隔板,那么他们变成了两团,但是单独考虑任何一团,其状态应该和放入隔板之前完全一致,这也意味着在 $p,T$ 固定的情况,$V$ 应该正比气体摩尔数 $\nu$ 。(题外话: 这个问题的反向问题,即合并两团气体非常经典,气体分子是否同类还会与是否会熵增有关,有兴趣可以查查吉布斯佯谬。)</p><p>因此,对于任意理想气体,我们可以得出 $\frac{pV}{\nu T} = const $ ,而右边这个常数记作 R ,为普适气体常数。<strong>理想气体方程常写作</strong>:</p><script type="math/tex; mode=display">pV = \nu RT</script><p>其中 $p,V,\nu,T$ 分别为气体压强、体积、摩尔数 和 绝对温度。</p><h1 id="物质与物态方程"><a href="#物质与物态方程" class="headerlink" title="物质与物态方程"></a>物质与物态方程</h1><p>一个热力学系统一般由很多参数来描述。对于简单的,无物质交换的体系,我们一般可以用 $p,V,T$ 三个参量来描述,而描述其的状态方程则可以记作 $f(p,V,T) = 0$,称作物态方程。其在研究具体的问题的时候非常重要,其展现了物体的状态参量之间内在的联系,反映的是物体本身的性质。同样重要的还有几个与物态方程有关的物理量:</p><ul><li>体膨胀系数 $\alpha$</li></ul><script type="math/tex; mode=display">\alpha = \frac1{V}(\frac{\partial V}{\partial T})_p</script><ul><li>压强系数 $\beta$</li></ul><script type="math/tex; mode=display">\beta = \frac1{p}(\frac{\partial p}{\partial T})_V</script><ul><li>定温压缩系数 $\kappa$</li></ul><script type="math/tex; mode=display">\kappa = -\frac1{V}(\frac{\partial V}{\partial p})_T</script><p>由偏微分知识易知: $\alpha = \beta \kappa p$ 。此处给出一个不太严谨的证明: 将物态方程求一阶微分,得到 $ a \mathrm{d}p + b \mathrm{d}V + c \mathrm{d}T = 0$ 。然后用偏导数定义,代入即可。</p><p>下面将大致介绍一下比较基础的物态方程。</p><h2 id="气体"><a href="#气体" class="headerlink" title="气体"></a>气体</h2><p>理想气体物态方程便是前面已经介绍过的</p><script type="math/tex; mode=display">pV = \nu RT</script><p>但是真实世界的气体并不是这样的,为了更加准确的描述气体的行为,人们还找到了其他更加贴近真实气体的物态方程,其中最常见就是范德瓦尔兹方程。其为:</p><script type="math/tex; mode=display">(p + \frac{a\nu^2}{V^2})(V - \nu b) = \nu RT</script><p>其中 $a,b$ 为常量,其值取决于具体的气体。</p><p>事实上,理想气体方程、范式气体方程不仅是经验公式,其也可以由对应统计物理的理论在一定条件下导出。</p><h2 id="液体和固体"><a href="#液体和固体" class="headerlink" title="液体和固体"></a>液体和固体</h2><p>相较于气体,我们能很直观的感受到一个区别: 对于一个实心的固体/液体,我们很难对其进行压缩。这便是因为其定温压缩系数 $\kappa$ 以及 体膨胀系数 $\alpha$ 特别小,即我们需要更大的压强/温度才能将液体/固体压缩/膨胀到同样的程度。从微观上来看,这是因为液体/固体分子之间的空隙更加小了。因此,在一定的范围内,液/固体的状态方程中体积随压强/温度变化很小,而其非线性项就更小了,体积变化大致满足以下线性方程:</p><script type="math/tex; mode=display">V(T,p) \approx V(T_0,p_0) (1 + \alpha(T - T_0) - \kappa (p - p_0) )</script><script type="math/tex; mode=display">\frac{\Delta V}{V} \approx \alpha \Delta T - \kappa \Delta p</script><h2 id="顺磁性固体"><a href="#顺磁性固体" class="headerlink" title="顺磁性固体"></a>顺磁性固体</h2><p>*这部分需要一点点电磁学基础,但了解即可。</p><p>不仅传统的固液气可以作为一种物态,顺磁性的固体在外磁场中也可以看作一个热力学体系。此时,对应的状态参量 $p,V$ 则变为 $M,H$ ,对应的是固体的磁化强度和外界的磁场强度。实验测得顺磁性固体的物态方程大致可以写作:</p><script type="math/tex; mode=display">M = \frac{C}{T}H</script><p>上式为居里定律,是一种类似理想气体模型的公式。对应的,还有对居里定律修正的居里-外丝公式</p><script type="math/tex; mode=display">M = \frac{C}{T - \theta}H</script><p>上述两个公式同样可以运用一些统计物理理论导出。</p><h2 id="光子气体"><a href="#光子气体" class="headerlink" title="光子气体"></a>光子气体</h2><p>是的。你没有看错。光子也可以看作是一团气体!由纯光子构成的体系也可以具有物态方程。实际上,光子气体是<del>物竞</del>物理学里面很经典的模型。</p><p>对于光子气体,其物态方程为</p><script type="math/tex; mode=display">p = \frac{4\sigma}{3c} T^4</script><p>其中 $c$ 为光速,$\sigma$ 是黑体辐射中的斯忒蕃-玻尔兹曼常数。</p><h1 id="麦克斯韦速率分布"><a href="#麦克斯韦速率分布" class="headerlink" title="麦克斯韦速率分布"></a>麦克斯韦速率分布</h1><p>前文讲到,在大量微粒的情况下,统计规律几乎完全决定了宏观性质。因此理论上,我们只需推导出一个微粒处于某种状态的概率,那么就能很好的描述该体系的宏观表现。而麦克斯韦速度分布律就是对于分子运动速度的刻画。</p><p>首先,从最简单的入手。考虑一个离散的、只和单变量 $x$ 相关的概率分布函数的时候,我们可能会这样描述 “x 为 0 的概率为 10% , x 为 1 的概率为 40% , x 为 3 的 概率为 30% , x 为 4 的概率为 20 % 。”。而这样的描述法可以记作一个数列,即 $a_n$ 为 $x = n$ 发生的概率,且满足归一化条件</p><script type="math/tex; mode=display">\sum_{i = 0}^{+\infty}a_i = 1</script><p>类比数列到函数的推广,我们可以将离散的概率函数推广为连续函数 $f(x)$ ,其中处在 $x$ 位置的概率就是 $f(x)dx$ 。相应的,归一化条件就转变为(积分上下限取决于情况):</p><script type="math/tex; mode=display">\int_{-\infty}^{+\infty} {f(x)\mathrm{d}x} = 1</script><p>然后,我们应当认识一个新的概念叫做速度空间,其不同于我们正常生活的三维空间,其三个方向的坐标分别为速度在三个方向上的分量 $v_x,v_y,v_z$ 。麦克斯韦速度分布律则是给出气体微粒速度为 $\vec{v} = (v_x,v_y,v_z)$ 时的概率函数 $ f(v_x,v_y,v_z) $ 。此时,处于速度 $\vec{v}$ 的概率就是 $ f(v_x,v_y,v_z) \mathrm{d}v_x \mathrm{d}v_y \mathrm{d}v_z $</p><p>通过统计物理的知识,我们可以导出理想气体的麦克斯韦速率分布,由于篇幅限制<del>作者懒得写</del>,本文将不会介绍如何严格推导,这里给出一种简单但不本质的推导:</p><p>因为理想气体忽略气体分子之间的相互作用(即前文说的气体足够稀薄的前提),所以当分子速度为 $\vec{v}$ 的时候,其能量为 $ \frac{mv^2}{2} $ ,其中 $m$ 为分子质量。因此,借助<strong>玻尔兹曼分布</strong>,微粒在能量为 E 的状态的概率正比于 $\exp(\frac{-E}{kT})$ ,其中 $k$ 为玻尔兹曼常数。因此,可以设 $f(\vec{v}) = C \exp(-\frac{mv^2}{2kT}) $</p><p>最后归一化,确定常数可得公式:</p><script type="math/tex; mode=display">f(\vec{v}) = (\frac{m}{2\pi kT}) ^ {3/2} \mathrm{e}^{-\frac{mv^2}{2kT}}</script><p>由三个方向运动的独立性,速度概率密度函数也可以写作:</p><script type="math/tex; mode=display">f(\vec{v}) \mathrm{d}v_x \mathrm{d}v_y \mathrm{d}v_z =f_x(v_x) \mathrm{d}v_x\cdot f_y(v_y) \mathrm{d}v_y \cdot f_z(v_z) \mathrm{d}v_z</script><p>需要注意的是,应当区分<strong>速度分布概率密度</strong>和<strong>速率分布概率密度</strong>。前者是 $f(\vec{v})$ ,而后者一般记作 $F(v)$。速率是不含有方向的,而且速率为 $v$ 的情况可以看作速度空间中的一个半径为 $v$ 的薄球壳,其面积为 $4\pi v^2$,因此,应当满足 $F(v) = f(\vec{v}) \cdot 4\pi v^2$。因此,速率分布函数为:</p><script type="math/tex; mode=display">F(v) = 4\pi v^2 f(\vec{v}) = 4\pi (\frac{m}{2\pi kT}) ^ {3/2}v^2 \mathrm{e}^{-\frac{mv^2}{2kT}}</script><p>容易验证其也满足归一性即:</p><script type="math/tex; mode=display">\int_{0}^{\infty} F(v) \mathrm{d}v = 1</script><p>*笔者习惯记 $\alpha = \frac{m}{2kT} $ ,并将原式写成根据启发性的形式(或许):</p><script type="math/tex; mode=display">F(v) = \frac4{\sqrt\pi} \alpha^{3/2} \mathrm{e} ^{-\alpha v^2} v^2</script><p>借助麦克斯韦速度分布以及速率分布,我们可以得到一些重要数值,例如平均速率 $\overline{v} = \frac2{\sqrt{\pi\alpha}} = \sqrt{\frac{8kT}{\pi m}} $ ,气体方均速率 $v_{rms} = \sqrt{\overline{v^2}} = \sqrt{\frac{3kT}{m}}$,可用于推导单原子气体理想气体内能,结合泻流推导气体压强公式等等。</p><p>*如果您真的很认真在看并且更加认真的去算了,你会发现前面有几个定积分并不能通过求出原函数来计算,这就涉及一些奇怪的积分技巧了,在文章的最后会讲到的啦!你先别急。这是物理复习,不是数学复习。</p><h1 id="热力学第一定律相关"><a href="#热力学第一定律相关" class="headerlink" title="热力学第一定律相关"></a>热力学第一定律相关</h1><p>热力学第一定律本质是就是能量守恒定律,其描述为 : 体系内能的增加量 等于 外界对其做的功 以及 体系从外界吸热的总和。</p><h2 id="广义功"><a href="#广义功" class="headerlink" title="广义功"></a>广义功</h2><p>*(注:在本小节中,您可能好奇为啥笔者将 $\mathrm{d}$ 和 $\delta$ 混用 ,后面讲内能的时候会阐明原因)</p><p>在力学里面,功被定义为力对位移的积分。对于一个体系,我们同样可以用类似的方法来拓展定义功,例如在气体系统,在面积微元 $dA$ 上发生了 $\mathrm{d}x$ 的位移(定义向外为正),那么外界做功 $ \delta W = F \cdot \mathrm{d}x = -p \cdot \mathrm{d}A \cdot \mathrm{d}x = -p \mathrm{d}V $。类似的,对于一个闭合电路体系,外界(电流源) 对一个电子元件在 $\mathrm{d} t$ 的时间内也会做功 $\delta W = UI\mathrm{d}t = U \mathrm{d}q$ 。</p><p>在以上的例子中,我们总是可以可以把所做的功 $\delta W$ 写成一个 $X \mathrm{d}Y $ 的形式。更进一步的,我们可以发现 $X$ 的量往往是一种表征强度的量(例如压力、电压、压强等等),而 $Y$ 则往往是一些可以线性的、可积累的量(例如位移、电荷量、体积变化)。事实上在物理上,这一类 $X$ 均是强度量,而 $Y$ 则是广度量,详细信息可以问<a href="https://baike.baidu.com/item/%E5%BC%BA%E5%BA%A6%E9%87%8F/9832051?fr=aladdin">度娘</a>,这里就不多介绍了。在热力学中,我们把这个 $X$ 定义为<strong>广义力</strong> ,$Y$ 定义为<strong>广义位移</strong>,而 $X \mathrm{d}Y $ 就是广义功。</p><h2 id="内能"><a href="#内能" class="headerlink" title="内能"></a>内能</h2><p>内能,记作 $U$ ,即为一个系统内部的能量。对于气体分子,其内能主要由分子动能和分子势能构成。而对于理想气体,其有”足够稀薄”的前提,因此不用考虑分子间作用力,就没有分子势能,内能完全由分子动能组成。</p><p>需要注意的是,<strong>内能作为一种能量,也是一种状态量,即内能可以由气体的某些状态参数决定</strong>,其可能包括压强、体积、温度、摩尔数等等。因此,在变化的过程中,其只和始末态有关,与中间的过程完全无关。正因如此,我们才能对内能做微分,即 $\mathrm{d}U$ 。而对于功 $W$ 和 热量 $Q$ ,其变化量取决于中间的过程,并不是一个状态量,因此只能用 $\delta U , \delta Q $ 来表示微小的改变量。</p><h2 id="准静态过程"><a href="#准静态过程" class="headerlink" title="准静态过程"></a>准静态过程</h2><p>准静态过程指的是,系统变化的过程中进行的足够缓慢,以至于其经历的每一个中间态都可以近似看作平衡态。需要注意的是: <strong>只有准静态才能在相图上画出来</strong> (例如 p-V,V-T 图等等)。</p><p>*笔者认为,我们也应当明确区分准静态过程和可逆过程。笔者理解的可逆过程 = 准静态过程 + 无损耗(例如摩擦力等耗散力)。首先,不平衡的过程必然会带来损耗,因此系统变化过程必须是准静态过程。其次,也不能有其他损耗,因为可逆过程要求过程可以反过来进行,但是损耗是不可逆的,其本质是一个总体系(所有外界 + 当前体系)熵增的过程,而熵增不可逆(后面热力学第二定律会讲到)。</p><h2 id="热一的公式表述"><a href="#热一的公式表述" class="headerlink" title="热一的公式表述"></a>热一的公式表述</h2><p>有了以上的前置知识以后,我们便可以把热力学第一定律写成如下的形式:</p><script type="math/tex; mode=display">\begin{aligned}\mathrm{d} U &= \delta W + \delta Q \\ &= -p_{外界}\mathrm{d}V + \delta Q \end{aligned}</script><p>其中 U 为体系内能,W为外界对其做功,Q为外界对其传递的热量。</p><h2 id="热量和热容"><a href="#热量和热容" class="headerlink" title="热量和热容"></a>热量和热容</h2><p>热量,在热力学第一定律中主要指的是因为温差热传递而被传递的能量,其是由温差驱动。(有一些小例外,比如摩擦生热)</p><p>热容,是指在一个过程中,体系每升高一定温度所改变的能量,记为:</p><script type="math/tex; mode=display">C = \lim_{\Delta T \rightarrow 0}{\frac{\Delta Q}{\Delta T}}</script><p>显然的,一般体系的热容是要正比于其内部摩尔数的,我们也因此可以定义摩尔热容:</p><script type="math/tex; mode=display">C_m = \frac{C}{\nu}</script><p>在热力学中,比较重要的两个热容量是定体热容 $C_V$ 和定压热容 $C_p$。在固定体积的情况下,做功量为0,结合热力学第一定律可得:</p><script type="math/tex; mode=display">C_V = (\frac{\partial U}{\partial T})_V</script><p>在固定压强的情况下,我们同样可以导出:</p><script type="math/tex; mode=display">C_p = (\frac{\partial U}{\partial T})_p + p(\frac{\partial V}{\partial T})_p</script><p>物理学家<del>为了好看</del>引入了新的状态参量焓,定义为:</p><script type="math/tex; mode=display">H = U + pV</script><p> 因为右边所有参数都是状态量,所以焓也是一个状态量。引入焓以后,定压热容可以记作:</p><script type="math/tex; mode=display">C_p = (\frac{\partial H}{\partial T})_p</script><h1 id="又见理想气体"><a href="#又见理想气体" class="headerlink" title="又见理想气体"></a>又见理想气体</h1><p>对于理想气体,在准静态过程中,其热力学第一定律可以表述为有:</p><script type="math/tex; mode=display">\mathrm{d}U = -p\mathrm{d}V + \delta Q</script><p>*如果你知道熵 $S$,那么其实可以将理想气体的热力学第一定律写为:</p><script type="math/tex; mode=display">\mathrm{d}U = -p\mathrm{d}V + T \mathrm{d}S</script><h2 id="理想气体的内能和焓"><a href="#理想气体的内能和焓" class="headerlink" title="理想气体的内能和焓"></a>理想气体的内能和焓</h2><p>对于理想气体,其内能 $U$ 完全由分子动能决定,前面的麦克斯韦分布律一章已经推导出 $\overline{v^2} = \frac{3kT}{m}$ ,其可以看作 $x,y,z$ 每个自由度分到了 $\frac{kT}2$ 的分子平动动能。对于单原子分子,其分子动能就等于 $\frac{3kT}{2}$ 。但是对于多个原子的分子,其还有更多的自由度(例如转动、振动等等),每个自由度也会类似速度,有自己的概率分布函数,而这也会带来转动动能、振动动能等其他动能。一般来说,双原子分子的分子动能等于 $\frac{5kT}{2}$ (多了转动两个自由度,仔细详细为啥转动是2个),多原子分子的分子动能一般等于 $3kT$ (一共三个转动自由度)。(*笔者补充: 在室温下其实分子的振动自由度并未完全解锁,而也不是每个自由度正好能分到 $\frac{kT}2$ 的动能,其是关于自由度解锁程度的一个函数,而自由度解锁程度和温度有关。以上表述非常业余,但笔者~懒得想~想不出更好的描述方法了).</p><p>由上,我们可以求出理想气体的内能。利用 $R = N_Ak$ 这一公式,我们可以得出对于单原子气体,其内能 $U = \frac32 \nu R T$ ,双原子 $\frac52 \nu R T$ ,多原子一般是 $3 \nu R T$ 。当然,具体还是以题目/给定条件为准。假设某理想气体分子自由度的个数为 $\alpha$,那么气体内能 $U = \frac\alpha2 \nu RT $,而焓 $H = (\frac\alpha2 + 1) \nu RT$ 。</p><p>可以看出,理想气体的内能 $U$ 只和气体的温度 $T$ 和气体摩尔数 $\nu$ 相关,和体积压强无关。因此,我们可以通过实验来验证真实气体是否是理想气体。若气体的内能和体积有关,那么该气体必然不可能是理想气体。然而,1852年焦耳和汤姆孙通过节流过程发现真实气体的内能的确是和体积有关的,否定了这真实气体是理想气体的可能。然而,很大的范围内,理想气体模型依然近似适用,其依然值得我们研究。</p><h2 id="理想气体的热容与过程"><a href="#理想气体的热容与过程" class="headerlink" title="理想气体的热容与过程"></a>理想气体的热容与过程</h2><p>由定体热容以及定压热容的定义,结合前文理想气体的内能公式,以及理想气体状态方程,我们不难求出:</p><script type="math/tex; mode=display">\begin{aligned}C_V &= \frac\alpha2 R \\C_p &= (\frac\alpha2 + 1)R\end{aligned}</script><p>更进一步地,我们还可以求出理想气体在各种情况下热容。我们引入一个概念叫做<strong>多方过程</strong>,其定义为 $pV^n = const$ 的一个过程。通过简单的微积分的知识,我们容易求出:</p><script type="math/tex; mode=display">C_n = C_V + \frac{R}{1 - n}</script><p>当 $n = 0$ ,为定压过程,即 $C_p = C_V + R$。</p><p>当 $n = \infty$ ,为定容过程,即 $C_V = C_V$。</p><p>当 $n = 1$ ,为定温过程,温度不变,热容无意义。</p><p>特别地,当 $n = \frac{R}{C_V} + 1 $ 其热容为0,表明该过程中没有热交换,属于 <strong>绝热过程</strong>,我们记此时的 $n$ 为泊松比,记作 $\gamma$,满足性质 $\gamma = \frac{C_p}{C_V}$</p><h1 id="Ending-附录"><a href="#Ending-附录" class="headerlink" title="Ending + 附录"></a>Ending + 附录</h1><p>由于笔者太懒了,暂时就写到这里了。计划下半部分介绍一下相变、卡诺热机、熵、热力学第二定律、以及一些偏统计物理的知识。</p><p>附: 麦克斯韦速率分布积分相关。</p><script type="math/tex; mode=display">F(v) = 4\pi (\frac{m}{2\pi kT}) ^ {3/2}v^2 \mathrm{e}^{-\frac{mv^2}{2kT}}</script><p>*笔者习惯记 $\alpha = \frac{m}{2kT} $ ,并将原式写成根据启发性的形式(或许):</p><script type="math/tex; mode=display">F(v) = \frac4{\sqrt\pi} \alpha^{3/2} \mathrm{e} ^{-\alpha v^2} v^2</script><p>在积分的时候,我们常常遇到一个如下形式的定积分(常数已经忽略)</p><script type="math/tex; mode=display">\int_{0}^{+\infty}{v^n\mathrm{e}^{-\alpha v^2} \mathrm {d}v}</script><p>方便起见,我们记录上式为 $I_n$ 。</p><p>当 $n$ 为奇数的时候,我们容易通过分部积分化为 $n = 1$ 的情况,而对于 $n = 1$,我们可以通过换元积分求出 $I_1 = \frac1{2\alpha} $ 。</p><p>比较困难的是,当 $n$ 为偶数的时候,分部积分会转化为 $n = 0$ 的情况,而当 $I_0$ 被积的函数没有初等的原函数。这就比较麻烦了。但是,我们依然可以巧妙地求出其值。我们可以考虑 ${I_0}^2$ 这个式子,其积分变元看作 $x,y$ ,如下:</p><script type="math/tex; mode=display">{I_0}^2 = \int_0^NaN\int_0^NaN \mathrm{e}^{-\alpha(x^2 + y^2)} \mathrm{d}x\mathrm{d}y</script><p>将 $x,y$ 看作平面上正交的两个方向,积分范围就是第一象限。将积分转化为极坐标下的形式,记 $x = r \cos\theta , y = r \sin\theta $ 。此时:</p><script type="math/tex; mode=display">\begin{aligned}{I_0}^2 &= \int_0^NaN\int_0^NaN \mathrm{e}^{-\alpha(x^2 + y^2)} \mathrm{d}x\mathrm{d}y \\& = \int_{\theta=0}^{\theta=\frac\pi2}\int_{r = 0}^{r = +\infty} \mathrm{e}^{-\alpha r^2} \mathrm{d}r \cdot r\mathrm{d}\theta \\& = I_1 \cdot \frac\pi 2 \end{aligned}</script><p>显然,$I_0$ 非负,因此,</p><script type="math/tex; mode=display">I_0 = \frac{\sqrt\pi}{2\sqrt\alpha}</script><p>*然而,在考试和比赛的时候,你可能根本没有时间去一次次的分布积分求解 $I_n$ 。笔者在这里提供一个可以简化的方法:</p><script type="math/tex; mode=display">\begin{aligned}\frac{\partial I_n}{\partial \alpha} &= \frac{\partial }{\partial \alpha}\int_{0}^{+\infty}{v^n\mathrm{e}^{-\alpha v^2} \mathrm {d}v} \\&= -\int_{0}^{+\infty}{v^{n + 2}\mathrm{e}^{-\alpha v^2} \mathrm {d}v} \\&= -I_{n+2}\end{aligned}</script><p>因此,我们可以借助 $I_{n+2} = \frac{\partial I_n}{\partial \alpha}$ 来更快地求出 $I_n$ 。</p><p>附积分表:</p><div class="table-container"><table><thead><tr><th>$n$</th><th>$I_n$</th><th>$n$</th><th>$I_n$</th></tr></thead><tbody><tr><td>0</td><td>$\frac{\sqrt\pi}{2\sqrt\alpha}$</td><td>1</td><td>$\frac{1}{2\alpha}$</td></tr><tr><td>2</td><td>$\frac{\sqrt\pi}{4\sqrt\alpha^3}$</td><td>3</td><td>$\frac{1}{2\alpha^2}$</td></tr><tr><td>4</td><td>$\frac{3\sqrt\pi}{8\sqrt\alpha^5}$</td><td>5</td><td>$\frac{1}{\alpha^3}$</td></tr></tbody></table></div><p>END!</p>]]></content>
<summary type="html">一些非常基础的热学基本知识。</summary>
<category term="物理" scheme="http://darksharpness.github.io/categories/%E7%89%A9%E7%90%86/"/>
<category term="热学" scheme="http://darksharpness.github.io/categories/%E7%89%A9%E7%90%86/%E7%83%AD%E5%AD%A6/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
<category term="物理" scheme="http://darksharpness.github.io/tags/%E7%89%A9%E7%90%86/"/>
<category term="热学" scheme="http://darksharpness.github.io/tags/%E7%83%AD%E5%AD%A6/"/>
</entry>
<entry>
<title>树链剖分入门教学</title>
<link href="http://darksharpness.github.io/treecut/"/>
<id>http://darksharpness.github.io/treecut/</id>
<published>2023-02-01T01:35:05.000Z</published>
<updated>2023-02-01T05:17:59.000Z</updated>
<content type="html"><![CDATA[<h1 id="核心思想"><a href="#核心思想" class="headerlink" title="核心思想"></a>核心思想</h1><h2 id="一些定义"><a href="#一些定义" class="headerlink" title="一些定义"></a>一些定义</h2><p> 树链剖分,简称树剖 <del>树分</del> <del>全称树学分析</del>,是一个基于树形结构和线段树的启发式算法。其核心思想是把一颗树剖分为若干条链条,每段链条独自内部线性连续,然后利用以上特性结合更复杂的数据结构进行维护。</p><p> 本文介绍的属于 “重链剖分”。其可以用来解决修改树上两点之间路径上每个点的权值、修改子树中每个点权值、以及在线查询以上两种权值的和/最大值等问题。</p><p> 重链剖分的基本性质是: 对于树上每个节点,额外记录哪一颗子树的节点最多(最重),将这颗子树的根节点记录为重儿子,即<strong>节点数最多的子树的根节点为重儿子</strong>。特别的,若一个节点有多个子树拥有最多的节点,那么随机选取其中一个的根节点作为重儿子即可。</p><p> 如下图:<br><img src="https://s2.loli.net/2023/02/01/Liv4lPmxjIgHyut.png" alt=""></p><p> 图中,我们能很直观的看出,节点1的重儿子就是6,节点2的重儿子是9,节点3的重儿子就是7。”重”的定义是与我们的直观完全符合的。</p><p> 我们再定义<strong>重边是子节点是重儿子的边,反之则为轻边</strong>。同样是上面这张图,图中,1-6,6-3,3-7,7-8,2-9便是重边,而其他都属于轻边。我们再称重边连成的链条为重链。如上图,重链就是1-6-3-7-8 和 2-9。特别的,我们认为4、5这样不与任何重边相连接的点,本身构成重链,即 4 和 5 也是重链。<strong>容易看出,重链之间由轻边连接,即重链必定为一条链,不会有分叉;且树上每个点属于且仅属于一条重链</strong>。</p><p> 在维护的时候,我们的核心思路就是在重链上一次性更新尽可能多的节点,因为在轻边上我们一次更新的节点数很少。根据这样的性质,我们应当将树上的节点重新编号,并且把重链上的节点编号连续化,以便于区间维护。因此,我们将用 dfs 时的先后顺序来为作为新编号 (记为dfn) ,在dfs的时候先走重儿子,再遍历其他子树,使得重链上的 dfn 必然是连续的。</p><h2 id="一些性质"><a href="#一些性质" class="headerlink" title="一些性质"></a>一些性质</h2><p> 考虑重链剖分的基本性质: 节点数最多的子树的根节点为重儿子。我们从整棵树的根节点出发向下走,走到一个目标节点。如果走重链,那么我们一次走到不能再走重边为止;如果走轻边,那么前往的子树的大小不会超过当前树大小的一半(重链剖分的性质),只会走一次。因此,我们总是在”走重链”和”走轻边”之间切换,而假设树有 $n$ 个节点,因为轻边的子树节点数不超过当前树的节点数的一半,所以我们走轻边的次数不会超过 $\log_2n$ 次,走重链的次数不会超过 $\log_2n + 1$ 次(因为轻-重交替)。因此,<strong>对于树上任意路径,轻边和重链的总数不会超过 $O(\log n)$ 数量级</strong>。</p><p> 再回到我们之前的 dfs 的方式,我们通过先访问重儿子使得<strong>重链上 dfn 连续</strong>,同时又由 dfs 的基本性质,在结束访问一个节点前,需要结束访问其子树上所有的节点。因此,对于一个 dfs 序为 dfn 的点,若其子树(包括自己这个节点)大小为size,则其和其子树的编号,正好覆盖 [dfn,dfn + size -1] 这个整数区间,即 <strong>子树的 dfn 连续</strong>。</p><h1 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h1><p> 首先,我们往往根据情况需要一个维护连续区间,支持区间查询的数据结构。一般来说,我们会对应选用 树状数组/线段树/分块 等数据结构来维护。为简化讨论,本文将统一使用线段树作为维护区间的数据结构。大致如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> <span class="type">int</span> N = <span class="number">1e5</span> + <span class="number">3</span>; <span class="comment">// 视题目而变</span></span><br><span class="line">dark::segment_tree <<span class="type">int</span>,N> t; <span class="comment">// 线段树</span></span><br></pre></td></tr></table></figure><p> 其次,我们要通过 dfs 处理出一些关键的信息,而大部分情况下,需要两次dfs。具体维护的信息请看代码注释:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* 注: 本文节点编号 1-base */</span></span><br><span class="line">vector <<span class="type">int</span>> g[N]; <span class="comment">// 用 vector 存图</span></span><br><span class="line"><span class="type">int</span> son[N]; <span class="comment">// 重儿子节点,必须维护</span></span><br><span class="line"><span class="type">int</span> top[N]; <span class="comment">// 重链最顶部的节点,必须维护</span></span><br><span class="line"><span class="type">int</span> fat[N]; <span class="comment">// 父亲节点,必须维护</span></span><br><span class="line"><span class="type">int</span> siz[N]; <span class="comment">// 子树大小,必须维护</span></span><br><span class="line"><span class="type">int</span> dep[N]; <span class="comment">// 节点深度,有需要就维护</span></span><br><span class="line"><span class="type">int</span> dfn[N]; <span class="comment">// dfs 序,必须维护</span></span><br><span class="line"><span class="type">int</span> rev[N]; <span class="comment">/*</span></span><br><span class="line"><span class="comment"> dfn 的反函数(reverse),即满足:</span></span><br><span class="line"><span class="comment"> dfn[rev[i]] = rev[dfn[i]] = i;</span></span><br><span class="line"><span class="comment"> 在有节点初始权值即线段树需要build的时候,才需要维护</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理出 son,fat,siz,dep信息</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">dfs1</span><span class="params">(<span class="type">int</span> u)</span> </span>{ <span class="comment">// u 为当前节点</span></span><br><span class="line"> <span class="type">int</span> maxsiz = <span class="number">-1</span>; <span class="comment">// 当前节点最大的子树大小</span></span><br><span class="line"> siz[u] = <span class="number">1</span>; <span class="comment">// 至少包含当前节点的 siz</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> v : g[u]) { <span class="comment">// C++11特性。此处用来遍历 u 所有边,v为指向的点</span></span><br><span class="line"> <span class="keyword">if</span>(v == fat[u]) <span class="keyword">continue</span>; <span class="comment">// 父节点不重复遍历</span></span><br><span class="line"> dep[v] = dep[u] + <span class="number">1</span>;</span><br><span class="line"> fat[v] = u;</span><br><span class="line"> <span class="built_in">dfs1</span>(v);</span><br><span class="line"> siz[u] += siz[v];</span><br><span class="line"> <span class="keyword">if</span>(siz[v] > maxsiz) {</span><br><span class="line"> maxsiz = siz[v];</span><br><span class="line"> son[u] = v;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="type">int</span> CNT = <span class="number">0</span>; <span class="comment">// dfn的总数</span></span><br><span class="line"><span class="comment">// 处理出 top,dfn,rev</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">dfs2</span><span class="params">(<span class="type">int</span> u,<span class="type">int</span> tp)</span> </span>{ <span class="comment">// u 同上,tp为当前重链顶部</span></span><br><span class="line"> dfn[u] = ++CNT;</span><br><span class="line"> rev[CNT] = u;</span><br><span class="line"> top[u] = tp;</span><br><span class="line"> <span class="keyword">if</span>(!son[u]) <span class="keyword">return</span>; <span class="comment">// 没有重儿子 = 没有儿子</span></span><br><span class="line"> <span class="built_in">dfs2</span>(son[u],tp); <span class="comment">// 先走重儿子</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> v : g[u]) { <span class="comment">// 同上</span></span><br><span class="line"> <span class="comment">// 重儿子走过了,父节点不走</span></span><br><span class="line"> <span class="keyword">if</span>(v == fat[u] || v == son[u]) <span class="keyword">continue</span>; </span><br><span class="line"> <span class="built_in">dfs2</span>(v,v); <span class="comment">// 走轻边,top改变</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h1><h2 id="在线求最近公共祖先-LCA问题"><a href="#在线求最近公共祖先-LCA问题" class="headerlink" title="在线求最近公共祖先(LCA问题)"></a>在线求最近公共祖先(LCA问题)</h2><p> 具体思路类似倍增求 LCA (如果你还不知道LCA以及其倍增解法,点击<a href="https://oi-wiki.org/graph/lca/">这里</a>),同样是从两个底部节点往上(根节点)跳寻找第一处祖先相同的地方。不同的是,在重链剖分的情况下,我们向上跳操作,走的是重链和轻边。假设当前有两个节点为 x 和 y,通过交换操作我们保证 dep[top[x]] 不超过 dep[top[y]] ,如果 top[x] 和 top[y] 不相等,说明 LCA 在必定在 top[x] 所在节点的上方,我们将 x 更新为 fat[top[x]],重复以上操作,直到 top[x] = top[y]。此时,x、y已经在同一条重链上,由 LCA 的知识易知,LCA就是 x 和 y 中深度更浅的那个点。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">getLCA</span><span class="params">(<span class="type">int</span> x,<span class="type">int</span> y)</span> </span>{</span><br><span class="line"> <span class="keyword">while</span>(top[x] != top[y]) {</span><br><span class="line"> <span class="keyword">if</span>(dep[top[x]] < dep[top[y]]) </span><br><span class="line"> y = fat[top[y]];</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> x = fat[top[x]];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> dep[x] < dep[y] ? y : x;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 该实现只需要 $O(n)$ 的预处理时间,以及 $O(\log n)$ 的单次查询时间,优于倍增法 $O(n\log n)$ 的预处理时间。而且,一般除了满二叉树、一直走轻边的情况,单次查询很难跑满 $ \log_2 n$ 的 “跳top” 上限次数,实际常数比较小。 </p><h2 id="维护路径和子树权值"><a href="#维护路径和子树权值" class="headerlink" title="维护路径和子树权值"></a>维护路径和子树权值</h2><p> 对于查询路径,重链剖分可以将其转化为一个区间维护问题。例题 <a href="https://www.luogu.com.cn/problem/P3384">Luogu3384</a>:</p><h3 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h3><p>如题,已知一棵包含 $N$ 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:</p><ul><li><p><code>1 x y z</code>,表示将树从 $x$ 到 $y$ 结点最短路径上所有节点的值都加上 $z$。</p></li><li><p><code>2 x y</code>,表示求树从 $x$ 到 $y$ 结点最短路径上所有节点的值之和。</p></li><li><p><code>3 x z</code>,表示将以 $x$ 为根节点的子树内所有节点值都加上 $z$。</p></li><li><p><code>4 x</code> 表示求以 $x$ 为根节点的子树内所有节点值之和</p></li></ul><h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><p> 前两个操作其实就是区间修改 + 查询路径权值和。通过树剖,我们可以将其划分为轻边和重链。类似前面求 LCA 的方法,我们在修改的时候依然使用向上跳的方式修改,对于每一次从 x 跳到 top[x] 再变为 fat[top[x]],其 dfn 是连续的。因此,我们可以直接在线段树上修改这一段连续区间。最后,如果 x 已经跳到了 LCA 这个点,x、y已经在一条重链上时,只需再把 dfn[x] 到 dfn[y] 这一段修改了即可。查询同理。此时,单次操作时间复杂度为 $O(\log^2 n)$。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">dark::segment_tree <<span class="type">int</span>,N> t; <span class="comment">// 线段树</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">addPath</span><span class="params">(<span class="type">int</span> x,<span class="type">int</span> y,<span class="type">int</span> val)</span> </span>{</span><br><span class="line"> <span class="keyword">while</span>(top[x] != top[y]) {</span><br><span class="line"> <span class="keyword">if</span>(dep[top[x]] < dep[top[y]]) <span class="built_in">swap</span>(x,y);</span><br><span class="line"> t.<span class="built_in">add</span>(dfn[top[x]],dfn[x],val); <span class="comment">// 区间加 </span></span><br><span class="line"> x = fat[top[x]];</span><br><span class="line"> } <span class="comment">/* 现在 top[x] == top[y] */</span></span><br><span class="line"> <span class="keyword">if</span>(dep[x] > dep[y]) <span class="built_in">swap</span>(x,y);</span><br><span class="line"> t.<span class="built_in">add</span>(dfn[x],dfn[y],val);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 类似,此处略去</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">sumPath</span><span class="params">(<span class="type">int</span> x,<span class="type">int</span> y)</span></span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p> 后面两个操作则是关于子树信息的维护。由于树剖以后,子树内部 dfn 连续,因此修改子树信息只需一次区间操作即可。单次操作时间复杂度为 $O(\log n)$</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">addTree</span><span class="params">(<span class="type">int</span> x,<span class="type">int</span> val)</span> </span>{</span><br><span class="line"> t.<span class="built_in">add</span>(dfn[x],dfn[x] + siz[x] - <span class="number">1</span>,val);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">sumTree</span><span class="params">(<span class="type">int</span> x)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> t.<span class="built_in">sum</span>(dfn[x],dfn[x] + siz[x] - <span class="number">1</span>);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p> 综上,经过了 $q$ 次操作以后,该程序的总时间复杂度为 $O(n + q\log^2 n) $。而事实上由于树剖本身常数小,其跑起来像是少了一个 log 一样(但是可以被满二叉树卡满)。</p><h1 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h1><p> 重链剖分是一种独特的数据结构,其通过启发式的划分轻重链并重新编号,使得重链上编号连续,进而将树上路径问题和子树问题问题转化为区间维护问题。其本体核心在于两次 dfs 以及跳 top 的一个过程。事实上写多了重链剖分,你就会发现大部分这方面的基础题主要考察如何转化模型为树剖,以及如何设计内部维护的数据结构(比如线段树、分块等等)。如果想做更多的题目巩固可以参考下 OIwiki 给出的<a href="https://oi-wiki.org/graph/hld/#%E7%BB%83%E4%B9%A0">练习</a> or <a href="https://conless.dev/">Conless</a> 的<a href="https://conless.dev/blog/cs/shulian-poufen/">学习计划</a>:</p><ul><li><a href="https://www.luogu.com.cn/problem/P3379">「luogu P3379」【模板】最近公共祖先(LCA)</a> (无需额外数据结构)</li><li><a href="https://loj.ac/p/2125">「HAOI2015」树上操作</a> </li><li><a href="https://www.luogu.com.cn/problem/P3384">「Luogu P3384」【模板】树链剖分</a></li><li><a href="https://www.luogu.com.cn/problem/P4427">「BJOI2018」求和</a></li><li><a href="https://acm.sjtu.edu.cn/OnlineJudge/problem?problem_id=1473">「ACMOJ 1473」</a></li><li><a href="https://www.luogu.com.cn/problem/P2486">「SDOI2011」染色</a></li><li><a href="https://www.luogu.com.cn/problem/P3979">「Luogu P3979」遥远的国度</a></li></ul>]]></content>
<summary type="html">一个基于树形结构和区间维护数据结构的启发式算法。</summary>
<category term="算法" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="树链剖分" scheme="http://darksharpness.github.io/categories/%E7%AE%97%E6%B3%95/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/%E6%A0%91%E9%93%BE%E5%89%96%E5%88%86/"/>
<category term="树形结构" scheme="http://darksharpness.github.io/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"/>
<category term="线段树" scheme="http://darksharpness.github.io/tags/%E7%BA%BF%E6%AE%B5%E6%A0%91/"/>
<category term="树链剖分" scheme="http://darksharpness.github.io/tags/%E6%A0%91%E9%93%BE%E5%89%96%E5%88%86/"/>
</entry>
<entry>
<title>C++ 部分特性梳理</title>
<link href="http://darksharpness.github.io/note/"/>
<id>http://darksharpness.github.io/note/</id>
<published>2023-01-30T07:09:04.000Z</published>
<updated>2023-03-12T09:31:24.000Z</updated>
<content type="html"><![CDATA[<p><del>咕咕咕,大概寒假结束前写完。欢迎催更。</del></p><p><del>寒假早结束了才发现还有一堆没写完</del></p><h1 id="基础整数类型"><a href="#基础整数类型" class="headerlink" title="基础整数类型"></a>基础整数类型</h1><p>C++ 默认的整数类型有很多,例如 short,int,long,long long,对应的都有 unsigned 的版本和 signed 版本,这是大家都所熟知的。值得特别注意的有以下几点:</p><ol><li>signed/unsigned 默认对应(等价)的是 int/unsigned int</li><li>char 不一定等于 signed char ! 尽管其他大部分整数类型不写 signed/unsigned 前缀默认的是 signed,但是 char 是一个特例。char 和 signed char 不一定是同一个类型,char 的具体实现取决于编译器。事实上,为了区分字符类型或最小寻址单位的 char 和 整数类型的 signed/unsigned char,这三者往往是互不相同的内置类型。(感兴趣的可以去查查 std::byte,其进一步细化了 char 的功能,std::byte 仅是用作最小寻址单元,不能四则运算,只能最简单的赋值位运算之类的)</li><li>即使两个内置的整数类型含二进制位数相同,其也属于不同的类。曾经愚蠢的 DarkSharpness 以为在 Win64 下 long 和 long long 都是 64bit,所以他们是同一个类型,但是他错了。即使其二进制位数相同,计算结果完全相同,甚至可以无开销地静态转化,其依然是两个不同的类型。</li></ol><p><img src="https://s2.loli.net/2023/03/12/K7RnAjB5eFX8lwN.png" alt="如图所示"></p><h1 id="指针与内存"><a href="#指针与内存" class="headerlink" title="指针与内存"></a>指针与内存</h1><p>指针是一个好东西。然而 DarkSharpness 过去被这玩意坑惨了。</p><p>指针本质上保存的是一块内存区域的地址,其具体是一个数字。在 delete 的时候,仅仅是把指向的这块内存区域给清空了,并不一定修改指针本身的值。因此,重复 delete 一个指针会出问题(注意,delete nullptr 貌似是不会出事的)。但坑人的不是这个,坑人的在于某些类的析构函数是不保证 delete 后把指针置为空的。因此,这会导致用户手动调用析构函数释放内存带来潜在的风险。以前 DarkSharpness 不懂(ICPC 大作业),愚蠢地手动调用 map 的析构函数,然后就 RE(Runtime Error)了。事实上,清空一个 map 容器比较安全的方法如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">std::map <<span class="type">int</span>,<span class="type">int</span>> recorder;</span><br><span class="line"><span class="comment">// Some function here. </span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">recorder = std::map <<span class="type">int</span>,<span class="type">int</span>>();</span><br></pre></td></tr></table></figure><p>在用临时变量进行右值赋值的时候,map 会先清空自身占用的内存空间,然后再直接接管该临时变量的内存空间(为空)。对于其他 STL 容器原理大致相同。</p><p>这个故事告诉我们,在不清楚内部实现的情况下,永远不要多次调用析构函数,其只保证对一个合法对象调用一次是不会有问题的。</p><h1 id="优化-忧化"><a href="#优化-忧化" class="headerlink" title="优化? 忧化!"></a>优化? 忧化!</h1><p>O2 优化是一个好东西,但是其可能导致一些 unexpected error。</p><p>例如以下这段代码: (from <a href="https://github.com/hsfzLZH1">hsfzLZH1</a>)</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> <span class="type">int</span> maxn = <span class="number">1e6</span> + <span class="number">3</span>;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">que</span>{</span><br><span class="line"> <span class="type">int</span> l,r,*s;</span><br><span class="line"> <span class="built_in">que</span>() {l=<span class="number">1</span>;r=<span class="number">0</span>;s=<span class="keyword">new</span> <span class="type">int</span>[maxn];<span class="built_in">memset</span>(s,<span class="number">0</span>,<span class="keyword">sizeof</span> s);}</span><br><span class="line"> ~<span class="built_in">que</span>() { <span class="keyword">delete</span> []s; }</span><br><span class="line"> <span class="function"><span class="type">bool</span> <span class="title">empty</span><span class="params">()</span></span>{<span class="keyword">return</span> l>r;}</span><br><span class="line"> <span class="function"><span class="type">int</span> <span class="title">front</span><span class="params">()</span></span>{<span class="keyword">return</span> s[l];}</span><br><span class="line"> <span class="function"><span class="type">int</span> <span class="title">back</span><span class="params">()</span></span>{<span class="keyword">return</span> s[r];}</span><br><span class="line"> <span class="function"><span class="type">int</span> <span class="title">popfront</span><span class="params">()</span> </span>{l++;}</span><br><span class="line"> <span class="function"><span class="type">int</span> <span class="title">popback</span><span class="params">()</span> </span>{r--;}</span><br><span class="line">}q1,q2;</span><br></pre></td></tr></table></figure><p>乍一看,这个队列没啥问题,除了 popfront() 和 popback() 没有返回参数。然而,如果你用这样实现的 queue 去提交Luogu 上的单调队列模板题,并且开 O2 优化。恭喜你,你可能会收获一个五颜六色的结果 (RE WA TLE MLE 随机组合)。</p><p><img src="https://s2.loli.net/2023/03/12/tIQf1nlkxgBabLr.png" alt="如果是 ACMOJ,你会 RE "></p><p>原因很简单,对于一个有返回参数的函数,由于 O2 优化对于 Undefined Behaviour 会有非常逆天而激进的优化(比较常见的是把 UB 当作不可达分支,认为程序员会保证不会产生导致 UB 的 input),你将难以预测他会做些什么。本例中,就会出现 RE 等错误。</p><p>再分享一个梗图。</p><p><img src="https://s2.loli.net/2023/03/12/BZldImL63AQnsFc.png" alt="WTF is that!"></p><p>死循环也是一个类似的 UB,在 clang 的 O1 下被激进地优化掉了。但是由于 int main() 没有返回参数,所以 clang 又自然地把返回语句优化没了,所以在运行完 main() 后,其没有结束程序,而是继续 fall down 到了 unreachable().</p><p><img src="https://s2.loli.net/2023/03/12/izS8gbRTyMIteKO.png" alt="看眼汇编就知道咯,main 被优化空了"></p><p>所以,请尽量避免 undefined behaviour 的出现! 常见的 UB 可以参考 <a href="https://en.cppreference.com/w/cpp/language/ub">cppreference</a>。如果你是 gcc 用户,并且想要尽可能在编译器检测出更多的 UB,请添加 -Wall 指令。</p><p>省流: 拒绝 UB ,从你他做起。</p><h1 id="END"><a href="#END" class="headerlink" title="END?"></a>END?</h1><p>感谢观看,以后有想起什么的话还会再更新的。Special thanks: <a href="https://github.com/hsfzLZH1">hsfzLZH1</a> , and You !</p>]]></content>
<summary type="html">一些坑过 DarkSharpness 的C++特性</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/categories/C/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
<category term="C++" scheme="http://darksharpness.github.io/tags/C/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>STL_vector简析</title>
<link href="http://darksharpness.github.io/vector/"/>
<id>http://darksharpness.github.io/vector/</id>
<published>2023-01-23T05:30:10.000Z</published>
<updated>2023-01-25T08:43:55.000Z</updated>
<content type="html"><![CDATA[<p> 本文将更加细致的分析 std::vector 的具体原理以及实现。关于vector 的基础功能,请点击<a href="https://darksharpness.github.io/2022/10/23/%E6%B5%85%E8%B0%88C++STL/STL/#std-vector">这里</a>。需要注意的是,vector <bool> 是一个特化的模板,其实现不同于其他的vector类,本文将不会讨论这一个特殊的实现,感兴趣可自行查询相关信息: <a href="https://en.cppreference.com/w/cpp/container/vector_bool">cppreference</a></p><p> 本文重点分析 vector 的底层实现以及其运用到的 C++ 11 以后的特性等等,会讲到一部分但不是全部vector的功能,对于 OIer 可能帮助不大,这里先提醒一下。</p><h1 id="std-vector-的功能"><a href="#std-vector-的功能" class="headerlink" title="std::vector 的功能"></a>std::vector 的功能</h1><h2 id="模板和声明"><a href="#模板和声明" class="headerlink" title="模板和声明"></a>模板和声明</h2><p> 首先,我们可以在 <code>#include<vector></code> 之后,通过 <code>std::vector <T> vec</code> 来声明一个存储了 T 类型变量的动态数组,初始为空。其中,用户还可以提供一个 allocator type 作为模板的第二个参数,但是一般情况下不会用到,本文也不会分析这一个参数。</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><p> vector 不仅可以初始化为空,也可以用同类型的vector初始化。除此之外,vector还支持初始化列表(要求C++11),也可以在构造的时候直接设置初始数组的大小以及元素的值。如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><vector></span></span></span><br><span class="line"><span class="keyword">using</span> std::vector;</span><br><span class="line"></span><br><span class="line">vector <<span class="type">int</span>> vec0; <span class="comment">// 空vector</span></span><br><span class="line">vector <<span class="type">int</span>> <span class="built_in">vec1</span>(<span class="number">10</span>); <span class="comment">// 初始数组长度为10的vector,这10个元素将分别调用默认无参数构造函数(对于int就是初始化为0)</span></span><br><span class="line">vector <<span class="type">int</span>> <span class="built_in">vec2</span>(<span class="number">10</span>,<span class="number">-1</span>); <span class="comment">// 初始化长度为10的vector,这10个元素初始化的值为-1</span></span><br><span class="line">vector <<span class="type">int</span>> vec3 = {<span class="number">1</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>}; <span class="comment">// 初始化为一个元素为1,3,4,5的数组</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// vector <double> vec4 = vec0; 不可以! 类型不同</span></span><br><span class="line">vector <<span class="type">int</span>> vec4 = vec0; <span class="comment">// 同类型vector 也可用于初始化</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="常用功能"><a href="#常用功能" class="headerlink" title="常用功能"></a>常用功能</h2><p> vector 的功能有很多。最基础的就是通过成员函数push_back() 或 emplace_back() 往 vector 尾部添加一个元素,push_back() 仅支持一个元素作为参数,而emplace_back() 可以传入多个参数,把这些参数作为新添加的元素的构造函数的参数。需要注意的是,我们应当区分 emplace_back() 和 push_back()的本质,两者虽然都是往尾部添加一个元素,但是底层机制不太一样,笔者将会在后文着重讨论。当然,有push_back()也自然会有pop_back()函数。</p><p> 类似原生数组,vector 也可以通过下标访问元素,进行读/写操作。而对于下标越界的情况,直接通过下标访问并不会进行任何检查,带来一些未定义行为。如果你的确需要含有越界检查的访问,请使用at()成员函数,其对于下标越界的情况会抛出错误。vector也可以用back()和front()成员函数访问最后一个/第一个元素,并且是一个读/写访问。当然,你也可以通过data()直接的得到vector的第一个元素的地址。由于vector内部数据是连续储存,这相当于就是指向vector内部数组的指针,但是该指针在vector进行改变容量的操作后会失效。</p><h2 id="迭代器"><a href="#迭代器" class="headerlink" title="迭代器"></a>迭代器</h2><p> vector也支持迭代器,既有成员函数版本的begin(),end(),cbegin(),cend(),也有非成员函数版的。也有反向迭代器 (即在正向迭代器前面加一个r,例如rbegin(),crend())。同时,其内存连续存储,所以其也是随机访问迭代器。</p><h2 id="容积相关"><a href="#容积相关" class="headerlink" title="容积相关"></a>容积相关</h2><p> vector还有许多关于容量的成员函数。首先是empty()用于判断vector()是否为空,size()返回 vector 内部元素的个数。除了以上两个常用的函数,其实还有一个capacity(),其返回的是,vector 已经申请的空间中可以存放元素的个数。这点可能比较绕,笔者将举例子阐释。例如 <code>class T</code> 在内存中占有字节数 $32$ Byte,vector 内部假设已经有了 $3$ 个 T 类型的元素,但是已经了申请 $128=32 \cdot 4$ Byte 的空间,那么其size()毫无疑问就是3,而capacity() 就是4。显然的,一个 vector 内部申请的可以存放元素的个数不少于其已经存放元素的个数。当然,用户不用担心这点,vector 内部会自动动态扩容的。</p><h2 id="元素相关"><a href="#元素相关" class="headerlink" title="元素相关"></a>元素相关</h2><p> vector 还有很多的管理内部元素的成员函数。clear()可以清空所有的元素 ;insert() , emplace() 可以在指定位置(用迭代器表示) 插入一个元素 ;erase() 则类似的可以在指定位置删除元素 ;assign()可以重新初始化 vector 内部的元素,统一赋值 ; resize() 可以改变 vector 的 size() ;reserve() 可以为 vector 预留一定的空间 ;;shrink_to_fit() 可以把capacity()降低到和size()一样 ;swap() 可以交换两个同类型 vector 内部的元素,并且只要常数的时间复杂度。附代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">vector <<span class="type">int</span>> vec = {<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>};</span><br><span class="line"><span class="comment">/* vec : {1,2,3} */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">clear</span>(); <span class="comment">// 清空vector</span></span><br><span class="line"><span class="comment">/* vec : {} */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> iter = vec.<span class="built_in">end</span>(); <span class="comment">// 指向尾部的 iterator</span></span><br><span class="line">vec.<span class="built_in">insert</span>(iter,<span class="number">1</span>); <span class="comment">// 在iter 前面插入一个元素</span></span><br><span class="line"><span class="comment">/* vec : {1} , size : 1 , capacity : 1 */</span></span><br><span class="line"></span><br><span class="line">iter = vec.<span class="built_in">begin</span>(); <span class="comment">// 注意,vector 元素个数改变后可能会导致迭代器失效! 请及时更新迭代器。</span></span><br><span class="line">vec.<span class="built_in">emplace</span>(iter,<span class="number">3</span>); <span class="comment">// 在iter 前面,用一系列参数作为构造函数构造一个新的元素,类似 emplace_back() </span></span><br><span class="line"><span class="comment">// (insert 和 emplace 具体差别请参考 push_back与emplace_back)</span></span><br><span class="line"><span class="comment">/* vec : {3,1} , size : 2 , capacity : 2 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">erase</span>(vec.<span class="built_in">end</span>()); <span class="comment">// 删除迭代器指向的的元素</span></span><br><span class="line"><span class="comment">/* vec : {3} , size : 1 , capacity : 2 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">resize</span>(<span class="number">3</span>); <span class="comment">// 将vec size()设置为3,如果新的size()大于原来的的size() , 新的元素将会采取默认构造函数</span></span><br><span class="line"><span class="comment">/* vec : {3,0,0} , size : 3 , capacity : 3 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">resize</span>(<span class="number">4</span>,<span class="number">1</span>) <span class="comment">// 类似,但是新的元素用传入的第二个参数拷贝构造</span></span><br><span class="line"><span class="comment">/* vec : {3,0,0,1} , size : 4 , capacity : 4 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">resize</span>(<span class="number">2</span>);</span><br><span class="line"><span class="comment">/* vec : {3,0} , size : 2 , capacity : 4 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">shrink_to_fit</span>(); <span class="comment">// 把多余的 capacity 扔掉</span></span><br><span class="line"><span class="comment">/* vec : {3,0} , size : 2 , capacity : 2 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">reserve</span>(<span class="number">3</span>); <span class="comment">// 预留一定空间,若小于当前capacity()则什么都不做</span></span><br><span class="line"><span class="comment">/* vec : {3,0} , size : 2 , capacity : 3 */</span></span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">assign</span>(<span class="number">4</span>,<span class="number">1</span>); <span class="comment">// 强行把vector的size()变为4,并且每个元素值等于第二个参数(如果没有就调用默认构造函数)</span></span><br><span class="line"><span class="comment">/* vec : {1,1,1,1} , size : 4 , capacity : 4 */</span></span><br><span class="line"></span><br><span class="line">vector <<span class="type">int</span>> tmp = {<span class="number">2</span>,<span class="number">3</span>,<span class="number">3</span>};</span><br><span class="line">tmp.<span class="built_in">swap</span>(vec); <span class="comment">// 常数时间交换两个 vector 元素</span></span><br><span class="line"><span class="comment">/* vec : {2,3,3} , size : 3 , capacity : 3 */</span></span><br><span class="line"><span class="comment">/* tmp : {1,1,1,1} , size : 4 , capacity : 4 */</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p> 最后,vector 还有一些常见的运算符操作,例如 = 赋值以及 == , < , > 等基本逻辑运算符。</p><h1 id="std-vector-的底层原理"><a href="#std-vector-的底层原理" class="headerlink" title="std::vector 的底层原理"></a>std::vector 的底层原理</h1><p> vector 是一个动态的数组,拥有自动扩容的能力(push_back操作),而且具有均摊 $O(1)$ 的添加元素时间复杂度,常常用于各类程序中。</p><p> 从底层的角度来看,vector本质上是储存了三个指针,分别指向内存区域的头部、内存区域尾部 以及 元素存储的尾部。当然,也可以存储头部指针、size()、capacity() 代替。$</p><p> 之所以 vector 能够支持 $O(1)$ 的添加元素时间复杂度 ,是因为其采用了 “倍增” 的扩容方式。当 size() == capacity() 的时候 ,再push_back()必须要扩容,因为申请的内存空间不足了。而此时,如果只申请 size() + 1 个元素的空间,那么,下次push_back()又要申请一次。这么实现,push_back() $n$ 次就达到了 $O(n^2)$ 的时间复杂度了。而基于 “倍增” 的思想,我们每次push_back需要扩容的时候,扩大capacity()为size()的两倍(例外: 当size() == $0$ 的时候,capacity()就变为 $1$ )。这样子,假设push_back() $n$次,$ 2^{m-1} \le n \lt 2^m$,那么最多扩容 $m + 1$ 次,扩容的总时间复杂度为:</p><script type="math/tex; mode=display">O(\sum_{i=0}^{m}{2^i}) = O(2^{m+1}) = O(n)</script><p> 这样一来,$n$ 次扩容就只要均摊 $O(n)$ 的时间复杂度,而且最多只会用两倍的空间,这在时空复杂度上都是十分高效的。以上便是 std::vector 最根本的原理。</p><h1 id="std-vector-的实现细节"><a href="#std-vector-的实现细节" class="headerlink" title="std::vector 的实现细节"></a>std::vector 的实现细节</h1><h2 id="构造函数赋值和swap"><a href="#构造函数赋值和swap" class="headerlink" title="构造函数赋值和swap()"></a>构造函数赋值和swap()</h2><p> 由于是指针管理内部数据,因此交换两个容器就只需要交换3个指针即可,所以是常数时间复杂度。同时,在 C++ 11 以后,移动构造函数、移动赋值函数也只需要移动三个指针即可,因此也是常数时间复杂度,其十分高效。而拷贝构造、拷贝赋值等操作则是一个一个的拷贝元素,时间复杂度为线性。</p><h2 id="size-和-capacity"><a href="#size-和-capacity" class="headerlink" title="size() 和 capacity()"></a>size() 和 capacity()</h2><p> 前面有讲过,size() 返回的是内部存放的元素的个数,而capacity() 则是已经预留空间的元素数,也已经举例讲过。更详细的说,我们可以把 vector 内部看作维护了三个指针 head,tail,end。其中 head 指向申请的内存块的首地址,end 指向了申请的内存块的尾地址(0-base,所以其实是在最后一个可用元素后面一个的位置),tail 则是指向当前存放最后一个元素后面一个的位置。此时,size() 就等于 tail - head,而 capacity() 就等于 end - head,满足 head $\lt$ tail $\le$ end 。需要注意的是,[head,tail) 之间的元素都是执行过一次构造函数的,但是 [tail,end) 之间的元素的都是未初始化的,没有执行过构造函数。对于一些复杂类,这样的初始构造函数是十分重要的。</p><h2 id="push-back-和-emplace-back"><a href="#push-back-和-emplace-back" class="headerlink" title="push_back() 和 emplace_back()"></a>push_back() 和 emplace_back()</h2><p> 前面提到的 push_back() 和 emplace_back(),不同便是在于 emplace_back() 极大地利用了 C++ 11 的特性。push_back() 只能接受一个 T 类型参数,可以是左值或者右值。对于左值,新的元素将通过拷贝构造初始化;对于右值,将会执行移动构造函数来初始化新的元素。而emplace_back()可以接受多个参数,其可以通过完美转发,直接用这些参数作为新元素的构造函数,来构造尾部的新元素。</p><p> 两者在用 T 类型、单个参数初始化的时候表现几乎一致,但是在非 T 类型或者多个参数初始化的时候,emplace_back() 效率显著高于 push_back()。具体来说,如果往尾部添加一个左值 T 对象,那么两者完全一致,都是执行一次拷贝构造。同理,传入一个右值 T 类型的对象,那么两者也同样,都是执行一次移动构造。但是,当传入一系列参数作为新元素的构造参数时,push_back()需要显式的调用 T() 在外部构造,然后vector 内部还会移动构造一次。而emplace_back()则直接在内部构造,只有一次构造,可以省去一次移动构造。</p><p>下面将举例分析。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">temp</span> {</span><br><span class="line"> <span class="type">unsigned</span> <span class="type">long</span> <span class="type">long</span> x;</span><br><span class="line"> <span class="type">double</span> y;</span><br><span class="line"> <span class="type">char</span> ch[<span class="number">1024</span>];</span><br><span class="line"> <span class="built_in">temp</span>() = <span class="keyword">default</span>;</span><br><span class="line"> <span class="built_in">temp</span>(<span class="type">const</span> temp &) = <span class="keyword">default</span>;</span><br><span class="line"> <span class="built_in">temp</span>(temp &&) = <span class="keyword">default</span>;</span><br><span class="line"> <span class="built_in">temp</span>(<span class="type">const</span> <span class="type">char</span> *str,<span class="type">double</span> __y) {</span><br><span class="line"> x = <span class="built_in">strlen</span>(str);</span><br><span class="line"> <span class="built_in">strcpy</span>(ch,str);</span><br><span class="line"> y = __y;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">vector <temp> vec;</span><br><span class="line"></span><br><span class="line">vec.<span class="built_in">push_back</span>(<span class="built_in">temp</span>(<span class="string">"DarkSharpness"</span>,<span class="number">1.99</span>));</span><br><span class="line">vec.<span class="built_in">emplace_back</span>(<span class="string">"DarkSharpness"</span>,<span class="number">1.99</span>);</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p> 在以上这段代码中,push_back 非 T (本例中为 temp) 类的参数需要先构造出一个 T 类对象 ,而内部则用这个临时对象来移动构造出一个元素。而emplace_back 则直接在内部用这些参数执行构造函数。而如果 T 类对象的移动构造并不是非常高效,甚至可能等同拷贝构造(例如用固定长度的字符数组表示的字符串,正如本例子中的temp,那么此时,emplace_back便可以省去不必要的移动构造过程,大大提升效率。</p><p> 值得注意的是,emplace_back和push_back有强异常保证。这意味着当扩容过程中发生了异常,那么原来的 vector 不会有任何的改变。这看起来没什么,但是这可能会极大的影响到程序的效率。因为在扩容时,需要将老的数据移到新的内存。一般来说,老数据都是没用的,应该调用的是移动构造而不是拷贝构造。但是,如果移动构造函数没有 noexcept 标识符,那么如果在移动某个元素 x 到 y 的过程中发生了异常,x的部分元素已经被移动到了 y ,我们不知道当前移动了多少,不知道如何把这部分数据移回 x ,也不确定移动回去会不会再发生异常。因此,原来 x 元素损坏了,数据丢失。为了满足强异常保证,此时只能使用拷贝构造。注意到了这点,我们对于明显不会抛出异常的移动构造函数应该加上 noexcept,否则可能影响程序效率,特别是对于拷贝开销大而移动开销小的类。详见<a href="https://en.cppreference.com/w/cpp/container/vector/emplace_back">cppreference</a>。</p><p> 在 C++ 11 之后,笔者认为应当用 emplace_back()替代push_back()以追求更高的效率。如果要减少模板的实例化(emplace_back是模板)并且对象易于移动(例如 std::string),或者<br>追求旧版本的兼容性,那么再用push_back()。</p><h2 id="emplace-和-insert"><a href="#emplace-和-insert" class="headerlink" title="emplace() 和 insert()"></a>emplace() 和 insert()</h2><p> 两者都是在指定的 iterator 前面插入一个元素,具体区别类似 emplace_back() 与 push_back()。当元素在尾部的时候,其同样有一定的强异常保证,即在 vector 尾部插入元素的异常表现完全等同 emplace_back() 与 push_back()。复杂度也是线性的,而且有多种变种,具体请参考cppreference: <a href="https://en.cppreference.com/w/cpp/container/vector/insert">insert</a>,<a href="https://en.cppreference.com/w/cpp/container/vector/emplace">emplace</a>。</p><h2 id="reserve-和-resize"><a href="#reserve-和-resize" class="headerlink" title="reserve() 和 resize()"></a>reserve() 和 resize()</h2><p> 这两个函数比较特殊,其均有改变 vector 的的大小的作用,只不过 reserve() 会预留空间,但是resize() 在预留空间(如果是扩容)的同时,还会对超过原来size()部分新建的元素执行构造函数。</p><p> 很多时候,当用户能明确得知 vector 大小的时候,的确需要 resize() 或 reserve()。然而,并不是所有时候都需要用resize() 或 reserve() 。因为两者会精确改变 vector 的 capacity() ,这很可能使得 vector 高效 push_back() 的特性失效。举例:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">vector <<span class="type">int</span>> vec = {<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>};</span><br><span class="line">vec.<span class="built_in">push_back</span>(<span class="number">1</span>);</span><br><span class="line">vec.<span class="built_in">push_back</span>(<span class="number">3</span>);</span><br><span class="line"><span class="comment">/* 只需要扩容一次,capacity = 6 */</span></span><br><span class="line"></span><br><span class="line">vector <<span class="type">int</span>> tmp = {<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>};</span><br><span class="line">tmp.<span class="built_in">resize</span>(tmp.<span class="built_in">size</span>() + <span class="number">1</span>);</span><br><span class="line">tmp.<span class="built_in">resize</span>(tmp.<span class="built_in">size</span>() + <span class="number">1</span>);</span><br><span class="line"><span class="comment">/* 需要扩容两次,capacity = 5 */</span></span><br></pre></td></tr></table></figure><p> 就笔者个人经验而言,一般情况下会在 vector 一开始的时候 reserve() 到最大可能的size() 然后 push_back() 不超过这个size() ; 或者 resize() 到指定大小,然后对小于size()的下标的元素,进行读/写操作,类似对一个定长 array 操作。总之,一般要避免连续的 reserve() 和 resize()。</p><h2 id="shrink-to-fit"><a href="#shrink-to-fit" class="headerlink" title="shrink_to_fit()"></a>shrink_to_fit()</h2><p> 这是一个比较玄学的函数,用来使 capacity() 缩小到 size(),减小空间占用。除非你明确释放内存(比如之后的vector的 size() 不会再增长只会减小,并且想要节约空间),否则请不要使用这个函数,其会使得 vector 的优化(即多预留空间)带来的高效率 push_back() 失效。</p><h2 id="clear"><a href="#clear" class="headerlink" title="clear()"></a>clear()</h2><p> 这个函数用来清空 vector 内部的元素,它会使得 size() 减小到0,但是 capacity() 不变,这是为了保证高效的 push_back() 操作。参考之前的实现,有一小部分用户 (比如曾经的我) 可能错以为 clear() 就是移动一个指针事情(tail = head) ,以为是常数复杂度。这样的观点是错误的。事实上,vector的clear()操作其实是线性复杂度,正比于内部元素个数。这是因为内部的元素可能是非平凡类,这种时候内部元素在销毁的时候必须执行析构函数(例如 std::map,析构函数需要释放内部的内存,不然会内存泄漏)。当然,这时候又有小可爱(比如我)想要问: 那么对于简单类,比如 int,double,其不需要析构函数来释放空间,那不是会降低效率吗。然而,cpp的编译器(至少gcc)要求了,对于空的析构(比如 ~int()),其会被优化掉,甚至连循环都会被优化掉。其会被优化为空,所以不用考虑这些细节对于效率的影响。</p><p><img src="https://s2.loli.net/2023/01/28/Z9kN6oHOedLfMBz.png" alt="写在gcc标准库注释里面的"></p><p><img src="https://s2.loli.net/2023/01/28/E2M41zGa3fyjQHL.png" alt="析构单个pointer"></p><h1 id="参考-vector-实现"><a href="#参考-vector-实现" class="headerlink" title="参考 vector 实现"></a>参考 vector 实现</h1><p> 笔者在寒假也写了一个类似 std::vector 的实现,其大致如下,具体可见 <a href="https://github.com/DarkSharpness/DarkSharpness/blob/main/Template/Dark/Container/dynamic_array.h">github仓库</a>。注意,没有任何可信的保证,程序可能存在很多 bug! 而且没有任何异常处理,push_back() emplace_back() 没有任何异常保证。总之,仅供学习、参考,没有任何保证! 以下是 2023/01/25 16:42 UTC+8 的参考版本,附加了一点点注释。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br><span class="line">399</span><br><span class="line">400</span><br><span class="line">401</span><br><span class="line">402</span><br><span class="line">403</span><br><span class="line">404</span><br><span class="line">405</span><br><span class="line">406</span><br><span class="line">407</span><br><span class="line">408</span><br><span class="line">409</span><br><span class="line">410</span><br><span class="line">411</span><br><span class="line">412</span><br><span class="line">413</span><br><span class="line">414</span><br><span class="line">415</span><br><span class="line">416</span><br><span class="line">417</span><br><span class="line">418</span><br><span class="line">419</span><br><span class="line">420</span><br><span class="line">421</span><br><span class="line">422</span><br><span class="line">423</span><br><span class="line">424</span><br><span class="line">425</span><br><span class="line">426</span><br><span class="line">427</span><br><span class="line">428</span><br><span class="line">429</span><br><span class="line">430</span><br><span class="line">431</span><br><span class="line">432</span><br><span class="line">433</span><br><span class="line">434</span><br><span class="line">435</span><br><span class="line">436</span><br><span class="line">437</span><br><span class="line">438</span><br><span class="line">439</span><br><span class="line">440</span><br><span class="line">441</span><br><span class="line">442</span><br><span class="line">443</span><br><span class="line">444</span><br><span class="line">445</span><br><span class="line">446</span><br><span class="line">447</span><br><span class="line">448</span><br><span class="line">449</span><br><span class="line">450</span><br><span class="line">451</span><br><span class="line">452</span><br><span class="line">453</span><br><span class="line">454</span><br><span class="line">455</span><br><span class="line">456</span><br><span class="line">457</span><br><span class="line">458</span><br><span class="line">459</span><br><span class="line">460</span><br><span class="line">461</span><br><span class="line">462</span><br><span class="line">463</span><br><span class="line">464</span><br><span class="line">465</span><br><span class="line">466</span><br><span class="line">467</span><br><span class="line">468</span><br><span class="line">469</span><br><span class="line">470</span><br><span class="line">471</span><br><span class="line">472</span><br><span class="line">473</span><br><span class="line">474</span><br><span class="line">475</span><br><span class="line">476</span><br><span class="line">477</span><br><span class="line">478</span><br><span class="line">479</span><br><span class="line">480</span><br><span class="line">481</span><br><span class="line">482</span><br><span class="line">483</span><br><span class="line">484</span><br><span class="line">485</span><br><span class="line">486</span><br><span class="line">487</span><br><span class="line">488</span><br><span class="line">489</span><br><span class="line">490</span><br><span class="line">491</span><br><span class="line">492</span><br><span class="line">493</span><br><span class="line">494</span><br><span class="line">495</span><br><span class="line">496</span><br><span class="line">497</span><br><span class="line">498</span><br><span class="line">499</span><br><span class="line">500</span><br><span class="line">501</span><br><span class="line">502</span><br><span class="line">503</span><br><span class="line">504</span><br><span class="line">505</span><br><span class="line">506</span><br><span class="line">507</span><br><span class="line">508</span><br><span class="line">509</span><br><span class="line">510</span><br><span class="line">511</span><br><span class="line">512</span><br><span class="line">513</span><br><span class="line">514</span><br><span class="line">515</span><br><span class="line">516</span><br><span class="line">517</span><br><span class="line">518</span><br><span class="line">519</span><br><span class="line">520</span><br><span class="line">521</span><br><span class="line">522</span><br><span class="line">523</span><br><span class="line">524</span><br><span class="line">525</span><br><span class="line">526</span><br><span class="line">527</span><br><span class="line">528</span><br><span class="line">529</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifndef</span> _DARK_DYNAMIC_ARRAY_H_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> _DARK_DYNAMIC_ARRAY_H_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"../include/basic.h"</span> <span class="comment">// 只用到了一个 Log2 函数,可以忽略</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">"../iterator"</span> <span class="comment">// 一个iterator 库</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><memory></span> <span class="comment">// std::allocator</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><initializer_list></span> <span class="comment">// 初始化列表</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> dark {</span><br><span class="line"></span><br><span class="line"><span class="comment">// dark::dynamic_array 就是 std::vector</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief A dynamic %array that can expand itself automatically.</span></span><br><span class="line"><span class="comment"> * In other words, user don't need to consider the allocation and </span></span><br><span class="line"><span class="comment"> * deallocation of space.</span></span><br><span class="line"><span class="comment"> * Note that if the elements within are pointers, the data pointed </span></span><br><span class="line"><span class="comment"> * to won't be touched. It's user's responsibility to manage them.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @tparam value_t The type of elements stored within the %array.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">value_t</span>></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">dynamic_array</span> : <span class="keyword">private</span> std::allocator <<span class="type">value_t</span>> {</span><br><span class="line"> <span class="keyword">protected</span>:</span><br><span class="line"> <span class="type">value_t</span> *head; <span class="comment">/* Head pointer to first element. */</span></span><br><span class="line"> <span class="type">value_t</span> *tail; <span class="comment">/* Tail pointer to one past the last element. */</span></span><br><span class="line"> <span class="type">value_t</span> *term; <span class="comment">/* Terminal pointer to the end of storage. */</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Allocate memory of __n elements. */</span></span><br><span class="line"> <span class="function"><span class="type">value_t</span> *<span class="title">alloc</span><span class="params">(<span class="type">size_t</span> __n)</span> </span>{ <span class="keyword">return</span> <span class="keyword">this</span>-><span class="built_in">allocate</span>(__n); }</span><br><span class="line"> <span class="comment">/* Deallocate of the memory of head. */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">dealloc</span><span class="params">()</span> </span>{ <span class="keyword">this</span>-><span class="built_in">deallocate</span>(head,<span class="built_in">capacity</span>()); }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Destory __n elements */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">destroy_n</span><span class="params">(<span class="type">value_t</span> *pointer,<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">while</span>(__n--) { <span class="keyword">this</span>-><span class="built_in">destroy</span>(pointer++); }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* End of unfolding. */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">reserved_push_back</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Push back a sequence of elements with space reserved in advance. */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">U</span>,<span class="keyword">class</span> ...Args></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">reserved_push_back</span><span class="params">(U &&obj,Args &&...objs)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::forward <U> (obj));</span><br><span class="line"> <span class="built_in">reserved_push_back</span>(std::forward <Args> (objs)...);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Construct a new empty %array. */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>() : <span class="built_in">head</span>(<span class="literal">nullptr</span>),<span class="built_in">tail</span>(<span class="literal">nullptr</span>),<span class="built_in">term</span>(<span class="literal">nullptr</span>) {}</span><br><span class="line"> <span class="comment">/* Destroy all the elements and deallocate the space. */</span></span><br><span class="line"> ~<span class="built_in">dynamic_array</span>() <span class="keyword">noexcept</span> { <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(head,<span class="built_in">size</span>()); <span class="built_in">dealloc</span>(); }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Construct a new %array from an initializer_list. */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>(std::initializer_list <<span class="type">value_t</span>> __l) </span><br><span class="line"> : <span class="built_in">dynamic_array</span>(__l.<span class="built_in">size</span>()) {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> &iter : __l) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::<span class="built_in">move</span>(iter)); }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Construct a new %array with __n elements' space reserved. */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>(<span class="type">size_t</span> __n) {</span><br><span class="line"> term = (head = tail = <span class="built_in">alloc</span>(__n)) + __n;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Construct a new %array filled with given length and element.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The initial length of the %array.</span></span><br><span class="line"><span class="comment"> * @param obj The element to fill the %array.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>(<span class="type">size_t</span> __n,<span class="type">const</span> <span class="type">value_t</span> &obj) </span><br><span class="line"> : <span class="built_in">dynamic_array</span>(__n) {</span><br><span class="line"> <span class="keyword">while</span>(tail != term) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,obj); }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Construct a new %array with identical elements with another %array.</span></span><br><span class="line"><span class="comment"> * Note that no vacancy of %array remains, </span></span><br><span class="line"><span class="comment"> * which means the new %array's size() equals its capacity(). </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param rhs The %array to copy from.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to the size() of rhs,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>(<span class="type">const</span> dynamic_array &rhs) </span><br><span class="line"> : <span class="built_in">dynamic_array</span>(rhs.<span class="built_in">size</span>()) {</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">const</span> <span class="keyword">auto</span> &iter : rhs) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,iter);}</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Construct a new %array with identical elements with another %array.</span></span><br><span class="line"><span class="comment"> * It will just take away the pointers from another %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param rhs The %array to move from.</span></span><br><span class="line"><span class="comment"> * @attention Constant time complexity in any case.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="built_in">dynamic_array</span>(dynamic_array &&rhs) <span class="keyword">noexcept</span> {</span><br><span class="line"> head = rhs.head;</span><br><span class="line"> tail = rhs.tail;</span><br><span class="line"> term = rhs.term;</span><br><span class="line"> rhs.head = rhs.tail = rhs.term = <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Copy assign a new %array with identical elements with another %array.</span></span><br><span class="line"><span class="comment"> * Note that no vacancy of %array remains, </span></span><br><span class="line"><span class="comment"> * which means the new %array's size() equals its capacity(). </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param rhs The %array to copy from.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to the size() of rhs,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element,</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> dynamic_array &<span class="keyword">operator</span> = (<span class="type">const</span> dynamic_array &rhs) {</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span> != &rhs) { <span class="built_in">copy_range</span>(rhs.<span class="built_in">begin</span>(),rhs.<span class="built_in">size</span>()); }</span><br><span class="line"> <span class="keyword">return</span> *<span class="keyword">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Construct a new %array with identical elements with another %array.</span></span><br><span class="line"><span class="comment"> * It will just move the pointers from another %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param rhs The %array to move from.</span></span><br><span class="line"><span class="comment"> * @attention Constant time complexity in any case.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> dynamic_array &<span class="keyword">operator</span> = (dynamic_array &&rhs) <span class="keyword">noexcept</span> {</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span> != &rhs) {</span><br><span class="line"> <span class="keyword">this</span>->~<span class="built_in">dynamic_array</span>();</span><br><span class="line"> head = rhs.head;</span><br><span class="line"> tail = rhs.tail;</span><br><span class="line"> term = rhs.term;</span><br><span class="line"> rhs.head = rhs.tail = rhs.term = <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> *<span class="keyword">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Swap the content of two %array in constant time. */</span></span><br><span class="line"> <span class="function">dynamic_array &<span class="title">swap</span><span class="params">(dynamic_array &rhs)</span> <span class="keyword">noexcept</span> </span>{</span><br><span class="line"> std::<span class="built_in">swap</span>(head,rhs.head);</span><br><span class="line"> std::<span class="built_in">swap</span>(tail,rhs.tail);</span><br><span class="line"> std::<span class="built_in">swap</span>(term,rhs.term);</span><br><span class="line"> <span class="keyword">return</span> *<span class="keyword">this</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/* Swap the content of two %array in constant time. */</span></span><br><span class="line"> <span class="function"><span class="keyword">friend</span> <span class="type">void</span> <span class="title">swap</span><span class="params">(dynamic_array &lhs,dynamic_array &rhs)</span> <span class="keyword">noexcept</span> </span>{</span><br><span class="line"> std::<span class="built_in">swap</span>(lhs.head,rhs.head);</span><br><span class="line"> std::<span class="built_in">swap</span>(lhs.tail,rhs.tail);</span><br><span class="line"> std::<span class="built_in">swap</span>(lhs.term,rhs.term);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line"> <span class="comment">/* Count of elements within the %array. */</span></span><br><span class="line"> <span class="function"><span class="type">size_t</span> <span class="title">size</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">return</span> tail - head; }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Count of elements the %array can hold </span></span><br><span class="line"><span class="comment"> * before the next allocation.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">size_t</span> <span class="title">capacity</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">return</span> term - head; }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Count of vacancy in the back of the %array </span></span><br><span class="line"><span class="comment"> * before the next allocation.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">size_t</span> <span class="title">vacancy</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">return</span> term - tail; }</span><br><span class="line"> <span class="comment">/* Test whether the %array is empty */</span></span><br><span class="line"> <span class="function"><span class="type">bool</span> <span class="title">empty</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">return</span> head == tail; }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Doing nothing to the %array. */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">push_back</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Push one element to the back of the %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param obj The object pushed back to initialize the element. </span></span><br><span class="line"><span class="comment"> * @attention Amortized constant time complexity,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">U</span>></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">push_back</span><span class="params">(U &&obj)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(tail == term) { <span class="built_in">reserve</span>(<span class="built_in">size</span>() << <span class="number">1</span> | <span class="built_in">empty</span>()); } </span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::forward <U> (obj));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Push a sequnce of elements to the back of the %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param objs The objects pushed back to initialize the element.</span></span><br><span class="line"><span class="comment"> * @attention Amortized linear time complexity with respect to count of objs,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> ...Args></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">push_back</span><span class="params">(Args &&...objs)</span> </span>{</span><br><span class="line"> <span class="type">size_t</span> count = <span class="keyword">sizeof</span>...(objs); <span class="comment">// count >= 2 </span></span><br><span class="line"> <span class="keyword">if</span>(<span class="built_in">vacancy</span>() < count) {</span><br><span class="line"> <span class="type">size_t</span> space = <span class="built_in">capacity</span>() + <span class="built_in">empty</span>();</span><br><span class="line"> <span class="built_in">reserve</span>(space << (<span class="built_in">LOG2</span>((<span class="built_in">size</span>() + count - <span class="number">1</span>) / space) + <span class="number">1</span>));</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">reserved_push_back</span>(std::forward <Args> (objs)...);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Construct one element after the back of the %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param obj The object pushed back to initialize the element. </span></span><br><span class="line"><span class="comment"> * @attention Amortized constant time complexity,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> ...Args></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">emplace_back</span><span class="params">(Args &&...objs)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(tail == term) { <span class="built_in">reserve</span>(<span class="built_in">size</span>() << <span class="number">1</span> | <span class="built_in">empty</span>()); }</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::forward <Args> (objs)...);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Destroy the last element in the back, with no returning. */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">pop_back</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>{ <span class="keyword">this</span>-><span class="built_in">destroy</span>(--tail); }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Clear all the elements in the %array.</span></span><br><span class="line"><span class="comment"> * Note that the capacity() of the %array won't shrink.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to size(),</span></span><br><span class="line"><span class="comment"> * multiplied by the deconstruction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">clear</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(head,<span class="built_in">size</span>());</span><br><span class="line"> tail = head;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Clear the vacancy of the %array.</span></span><br><span class="line"><span class="comment"> * Note that it will disable the optimization of the %array.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to size(),</span></span><br><span class="line"><span class="comment"> * multiplied by the deconstruction time of one element.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">shrink</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(tail != term) {</span><br><span class="line"> <span class="type">value_t</span> *temp = <span class="built_in">alloc</span>(<span class="built_in">size</span>());</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">size_t</span> i = <span class="number">0</span> ; i < <span class="built_in">size</span>() ; ++i) {</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">construct</span>(temp + i,std::<span class="built_in">move</span>(head[i]));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>->~<span class="built_in">dynamic_array</span>();</span><br><span class="line"> term = tail = temp + <span class="built_in">size</span>();</span><br><span class="line"> head = temp;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Resize the %array to __n.</span></span><br><span class="line"><span class="comment"> * The original data with index smaller than __n won't be touched. </span></span><br><span class="line"><span class="comment"> * If __n is greater than size(), elements will be appended to the back.</span></span><br><span class="line"><span class="comment"> * These new elements will be assigned by default construction function.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The new size of the %array.</span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to __n ,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">resize</span><span class="params">(<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">size</span>()) {</span><br><span class="line"> <span class="type">size_t</span> count = <span class="built_in">size</span>() - __n;</span><br><span class="line"> tail -= count;</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(tail,count);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">reserve</span>(__n);</span><br><span class="line"> <span class="type">size_t</span> count = __n - <span class="built_in">size</span>();</span><br><span class="line"> <span class="keyword">while</span>(count--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++); } </span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Resize the %array to __n.</span></span><br><span class="line"><span class="comment"> * The original data with index smaller than __n won't be touched. </span></span><br><span class="line"><span class="comment"> * If __n is greater than size(), elements will be appended to the back.</span></span><br><span class="line"><span class="comment"> * These new elements will be assigned by val.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The new size of the %array.</span></span><br><span class="line"><span class="comment"> * @param val The object to assign the new value.</span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to __n ,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">resize</span><span class="params">(<span class="type">size_t</span> __n,<span class="type">const</span> <span class="type">value_t</span> &val)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">size</span>()) {</span><br><span class="line"> <span class="type">size_t</span> count = <span class="built_in">size</span>() - __n;</span><br><span class="line"> tail -= count;</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(tail,count);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">reserve</span>(__n);</span><br><span class="line"> <span class="type">size_t</span> count = __n - <span class="built_in">size</span>();</span><br><span class="line"> <span class="keyword">while</span>(count--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,val); } </span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Reserve space for __n elements.</span></span><br><span class="line"><span class="comment"> * If __n < capacity(), nothing will be done.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The space reserved for elements.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to __n,</span></span><br><span class="line"><span class="comment"> * only if __n >= capacity(), multiplied by the time of (de-)construction. </span></span><br><span class="line"><span class="comment"> * Otherwise, constant time complexity.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">reserve</span><span class="params">(<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(<span class="built_in">capacity</span>() < __n) {</span><br><span class="line"> <span class="type">value_t</span> *temp = <span class="built_in">alloc</span>(__n);</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">size_t</span> i = <span class="number">0</span> ; i < <span class="built_in">size</span>() ; ++i) {</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">construct</span>(temp + i,std::<span class="built_in">move</span>(head[i]));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>->~<span class="built_in">dynamic_array</span>();</span><br><span class="line"> term = temp + __n;</span><br><span class="line"> tail = temp + <span class="built_in">size</span>();</span><br><span class="line"> head = temp;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Resize the %array to __n and assign all the elements by </span></span><br><span class="line"><span class="comment"> * default construction function of value_t. </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The new size of the %array.</span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to __n ,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">assign</span><span class="params">(<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="type">const</span> <span class="type">value_t</span> val = <span class="built_in">value_t</span>();</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">size</span>()) {</span><br><span class="line"> <span class="type">size_t</span> count = <span class="built_in">size</span>() - __n;</span><br><span class="line"> tail -= count;</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(tail,count);</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> &iter : *<span class="keyword">this</span>) { iter = val; }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> &iter : *<span class="keyword">this</span>) { iter = val; }</span><br><span class="line"> <span class="built_in">reserve</span>(__n);</span><br><span class="line"> <span class="type">size_t</span> count = __n - <span class="built_in">size</span>();</span><br><span class="line"> <span class="keyword">while</span>(count--) <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Resize the %array to __n and assign all the elements by val. </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param __n The new size of the %array.</span></span><br><span class="line"><span class="comment"> * @param val The object to assign the value.</span></span><br><span class="line"><span class="comment"> * @attention Linear complexity with respect to __n ,</span></span><br><span class="line"><span class="comment"> * multiplied by the construction time of one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">assign</span><span class="params">(<span class="type">size_t</span> __n,<span class="type">const</span> <span class="type">value_t</span> &val)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">size</span>()) {</span><br><span class="line"> <span class="type">size_t</span> count = <span class="built_in">size</span>() - __n;</span><br><span class="line"> tail -= count;</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(tail,count);</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> &iter : *<span class="keyword">this</span>) { iter = val; }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">auto</span> &iter : *<span class="keyword">this</span>) { iter = val; }</span><br><span class="line"> <span class="built_in">reserve</span>(__n);</span><br><span class="line"> <span class="type">size_t</span> count = __n - <span class="built_in">size</span>();</span><br><span class="line"> <span class="keyword">while</span>(count--) <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,val);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Copy elements from a range [first,last).</span></span><br><span class="line"><span class="comment"> * Note that the Iterator must be random access iterator.</span></span><br><span class="line"><span class="comment"> * Otherwise, you should provide the count of elements.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @tparam Iterator A random access iterator type.</span></span><br><span class="line"><span class="comment"> * @param first Iterator to the first element.</span></span><br><span class="line"><span class="comment"> * @param last Iterator to one past the last element.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to (last - first)</span></span><br><span class="line"><span class="comment"> * multiplied by the time of moving one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iterator</span>></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">copy_range</span><span class="params">(Iterator first,Iterator last)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">copy_range</span>(first,last - first);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Copy elements from a range [first,last).</span></span><br><span class="line"><span class="comment"> * The number of elements in the range should be exactly __n,</span></span><br><span class="line"><span class="comment"> * or unexpected error may happen.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param first Iterator to the first element.</span></span><br><span class="line"><span class="comment"> * @param last Iterator to one past the last element.</span></span><br><span class="line"><span class="comment"> * @param __n Count of all the elements in the range.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to __n,</span></span><br><span class="line"><span class="comment"> * multiplied by the time of copying one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iterator</span>></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">copy_range</span><span class="params">(Iterator first,<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">capacity</span>()) {</span><br><span class="line"> <span class="type">value_t</span> *temp = head;</span><br><span class="line"> <span class="keyword">while</span>(__n-- && temp != tail) { *(temp++) = *(first++); }</span><br><span class="line"> ++__n;</span><br><span class="line"> <span class="keyword">while</span>(__n--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,*(first++)); }</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(temp,tail - temp);</span><br><span class="line"> tail = temp;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>->~<span class="built_in">dynamic_array</span>();</span><br><span class="line"> term = (tail = head = <span class="built_in">alloc</span>(__n)) + __n;</span><br><span class="line"> <span class="keyword">while</span>(__n--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,*(first++)); } </span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Move elements from a range [first,last).</span></span><br><span class="line"><span class="comment"> * Note that the Iterator must be random access iterator.</span></span><br><span class="line"><span class="comment"> * Otherwise, you should provide the count of elements.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @tparam Iterator A random access iterator type.</span></span><br><span class="line"><span class="comment"> * @param first Iterator to the first element.</span></span><br><span class="line"><span class="comment"> * @param last Iterator to one past the last element.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to (last - first),</span></span><br><span class="line"><span class="comment"> * multiplied by the time of moving one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iterator</span>></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">move_range</span><span class="params">(Iterator first,Iterator last)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">move_range</span>(first,last - first);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * @brief Move elements from a range [first,last).</span></span><br><span class="line"><span class="comment"> * The number of elements in the range should be exactly __n,</span></span><br><span class="line"><span class="comment"> * or unexpected error may happen.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param first Iterator to the first element.</span></span><br><span class="line"><span class="comment"> * @param last Iterator to one past the last element.</span></span><br><span class="line"><span class="comment"> * @param __n Count of all the elements in the range.</span></span><br><span class="line"><span class="comment"> * @attention Linear time complexity with respect to __n,</span></span><br><span class="line"><span class="comment"> * multiplied by the time of moving one element.</span></span><br><span class="line"><span class="comment"> * Note that there might be an additional time cost linear to the </span></span><br><span class="line"><span class="comment"> * elements destroyed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">template</span> <<span class="keyword">class</span> <span class="title class_">Iterator</span>></span><br><span class="line"> <span class="function"><span class="type">void</span> <span class="title">move_range</span><span class="params">(Iterator first,<span class="type">size_t</span> __n)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span>(__n <= <span class="built_in">capacity</span>()) {</span><br><span class="line"> <span class="type">value_t</span> *temp = head;</span><br><span class="line"> <span class="keyword">while</span>(__n-- && temp != tail) { *(temp++) = std::<span class="built_in">move</span>(*(first++)); }</span><br><span class="line"> ++__n;</span><br><span class="line"> <span class="keyword">while</span>(__n--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::<span class="built_in">move</span>(*(first++))); }</span><br><span class="line"> <span class="keyword">this</span>-><span class="built_in">destroy_n</span>(temp,tail - temp);</span><br><span class="line"> tail = temp;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">this</span>->~<span class="built_in">dynamic_array</span>();</span><br><span class="line"> term = (tail = head = <span class="built_in">alloc</span>(__n)) + __n;</span><br><span class="line"> <span class="keyword">while</span>(__n--) { <span class="keyword">this</span>-><span class="built_in">construct</span>(tail++,std::<span class="built_in">move</span>(*(first++))); } </span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line"> <span class="comment">/* Return the pointer to the first element. */</span></span><br><span class="line"> <span class="function"><span class="type">value_t</span> *<span class="title">data</span><span class="params">()</span> </span>{ <span class="keyword">return</span> head; }</span><br><span class="line"> <span class="comment">/* Return the pointer to the first element. */</span></span><br><span class="line"> <span class="function"><span class="type">const</span> <span class="type">value_t</span> *<span class="title">data</span><span class="params">()</span> <span class="type">const</span> </span>{ <span class="keyword">return</span> head; }</span><br><span class="line"> <span class="comment">/* Subscript access to the data in the %array. */</span></span><br><span class="line"> <span class="type">value_t</span> &<span class="keyword">operator</span> [] (<span class="type">size_t</span> __n) { <span class="keyword">return</span> head[__n]; }</span><br><span class="line"> <span class="comment">/* Subscript access to the data in the %array. */</span></span><br><span class="line"> <span class="type">const</span> <span class="type">value_t</span> &<span class="keyword">operator</span> [] (<span class="type">size_t</span> __n) <span class="type">const</span> { <span class="keyword">return</span> head[__n]; }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Reference to the first element. */</span></span><br><span class="line"> <span class="function"><span class="type">value_t</span> &<span class="title">front</span><span class="params">()</span> </span>{<span class="keyword">return</span> *<span class="built_in">begin</span>();}</span><br><span class="line"> <span class="comment">/* Reference to the last element. */</span></span><br><span class="line"> <span class="function"><span class="type">value_t</span> &<span class="title">back</span><span class="params">()</span> </span>{<span class="keyword">return</span> *--<span class="built_in">end</span>();}</span><br><span class="line"> <span class="comment">/* Const reference to the first element. */</span></span><br><span class="line"> <span class="function"><span class="type">const</span> <span class="type">value_t</span> &<span class="title">front</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> *<span class="built_in">cbegin</span>();}</span><br><span class="line"> <span class="comment">/* Const reference to the last element. */</span></span><br><span class="line"> <span class="function"><span class="type">const</span> <span class="type">value_t</span> &<span class="title">back</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> *--<span class="built_in">cend</span>();}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">using</span> iterator = RandomAccess::iterator <<span class="type">value_t</span>>;</span><br><span class="line"> <span class="keyword">using</span> const_iterator = RandomAccess::const_iterator <<span class="type">value_t</span>>;</span><br><span class="line"> <span class="keyword">using</span> reverse_iterator = RandomAccess::reverse_iterator <<span class="type">value_t</span>>;</span><br><span class="line"> <span class="keyword">using</span> const_reverse_iterator = RandomAccess::const_reverse_iterator <<span class="type">value_t</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Iterator to the first element. */</span></span><br><span class="line"> <span class="function">iterator <span class="title">begin</span><span class="params">()</span> </span>{ <span class="keyword">return</span> head; }</span><br><span class="line"> <span class="comment">/* Iterator to one past the last element. */</span></span><br><span class="line"> <span class="function">iterator <span class="title">end</span><span class="params">()</span> </span>{ <span class="keyword">return</span> tail; }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Const_iterator to the first element. */</span></span><br><span class="line"> <span class="function">const_iterator <span class="title">begin</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> head;}</span><br><span class="line"> <span class="comment">/* Const_iterator to one past the last element. */</span></span><br><span class="line"> <span class="function">const_iterator <span class="title">end</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> tail;}</span><br><span class="line"> <span class="comment">/* Const_iterator to the first element. */</span></span><br><span class="line"> <span class="function">const_iterator <span class="title">cbegin</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> head;}</span><br><span class="line"> <span class="comment">/* Const_iterator to one past the last element. */</span></span><br><span class="line"> <span class="function">const_iterator <span class="title">cend</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> tail;}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Reverse iterator to the last element. */</span></span><br><span class="line"> <span class="function">reverse_iterator <span class="title">rbegin</span><span class="params">()</span> </span>{ <span class="keyword">return</span> tail - <span class="number">1</span>; }</span><br><span class="line"> <span class="comment">/* Reverse iterator to one before the first element. */</span></span><br><span class="line"> <span class="function">reverse_iterator <span class="title">rend</span><span class="params">()</span> </span>{ <span class="keyword">return</span> head - <span class="number">1</span>; }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* Const_reverse_iterator to the last element. */</span></span><br><span class="line"> <span class="function">const_reverse_iterator <span class="title">rbegin</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> tail - <span class="number">1</span>;}</span><br><span class="line"> <span class="comment">/* Const_reverse_iterator to one before the first element. */</span></span><br><span class="line"> <span class="function">const_reverse_iterator <span class="title">rend</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> head - <span class="number">1</span>;}</span><br><span class="line"> <span class="comment">/* Const_reverse_iterator to the last element. */</span></span><br><span class="line"> <span class="function">const_reverse_iterator <span class="title">crbegin</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> tail - <span class="number">1</span>;}</span><br><span class="line"> <span class="comment">/* Const_reverse_iterator to one before the first element. */</span></span><br><span class="line"> <span class="function">const_reverse_iterator <span class="title">crend</span><span class="params">()</span> <span class="type">const</span> </span>{<span class="keyword">return</span> head - <span class="number">1</span>;}</span><br><span class="line"></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">略深入地分析 std::vector</summary>
<category term="C++" scheme="http://darksharpness.github.io/categories/C/"/>
<category term="STL" scheme="http://darksharpness.github.io/categories/C/STL/"/>
<category term="STL" scheme="http://darksharpness.github.io/tags/STL/"/>
<category term="基础知识" scheme="http://darksharpness.github.io/tags/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
</entry>
<entry>
<title>2022学年上半学期总结</title>
<link href="http://darksharpness.github.io/summary/"/>
<id>http://darksharpness.github.io/summary/</id>
<published>2022-12-09T06:28:18.000Z</published>
<updated>2023-01-22T09:49:14.000Z</updated>
<content type="html"><![CDATA[<p>这学期几乎啥也没干,没啥好总结的。主要讲个人相关,会避免一些敏感话题。</p><h1 id="9月"><a href="#9月" class="headerlink" title="9月"></a>9月</h1><p> 最大的成就大概就是建了个blog,圆了OIer时代的旧梦。然而开学仅两周就遭到了疫情的迎头痛击,转线上课,而且还因为课程冲突导致”教师劫”没能回去看老师,哭。</p><p> 说实话,我个人是非常不喜欢线上上课的,因为我自律能力无,网课几乎就是摸鱼。不过线上课也是一个机遇,而且这段时间恰好是开学,学业负担 = 0。充足的时间使我有能力去补课一些OI算法(毕竟我在A班算法基础属于垫底水平orz),也能偶尔做做物理竞赛题目(怀念啊~),<del>还能有大把时间摸鱼电动,加上寝室车万浓度3/4直接爽上天</del>。当然,这段时间还顺便学了点设计傅里叶变换以及音频处理无关知识,作为副业说不定能赚点(笑)。</p><h1 id="10月"><a href="#10月" class="headerlink" title="10月"></a>10月</h1><p> 趁着国庆赶紧润回去了一趟,顺便初中同学聚会。然而回来后就开启了地狱模式,小作业大作业轮番轰炸,数分作业数量多到逆天(巅峰时一节课能布置26道题,一周3节课)。然后其他奇怪的课程的作业也接踵而至,不过到这里我还是勉强能应付的过来的,至少作业没有晚交缺交漏交,赢!。</p><h1 id="11月"><a href="#11月" class="headerlink" title="11月"></a>11月</h1><p> 大寄特寄的一个月,首先时学子讲坛和学术写作,好不容易水完了,然而后面两周就开始期中考试了,再然后int2048大作业还有python解释器的ddl也开始逼进,中间除了期中考试结束那一周,几乎没有休息的时间。</p><p> 这大概是我这学期最累的一段时间了,平均睡眠时间已经快要低于7.5个小时了,这是高中从没有的情况,这段时间我真的是天天晚上EMO发电。不过好在我心态比较好 <del>(非常佛系)</del> ,而且之前接触过大作业相关的一些东西(9月恰好学了点FFT,然后用在了int2048),苟到了月末,还抽空回去拿了冬装。</p><p> 总之,这是最累的一个月,这也是最充实的一个月,也是最快乐的一个月。</p><p><img src="https://s2.loli.net/2023/01/28/X1lnctef5WiRTpz.jpg" alt=""><br><img src="https://s2.loli.net/2023/01/28/KS8lOqFUw3XVfxL.jpg" alt="刚肝完学子讲坛、学术写作和检查点的我belike"><br><img src="https://s2.loli.net/2023/01/28/Go51y4sPWbJVqIR.jpg" alt="肝完python解释器后我杀疯的状态,你见过4 a.m.的OJ吗"><br><img src="https://s2.loli.net/2023/01/28/OjxiVsH6MvZyb9n.jpg" alt="可爱的"></p><h1 id="12月"><a href="#12月" class="headerlink" title="12月"></a>12月</h1><p> 虽然才过了几天,但啥事情都有(。印象很深的是12/1号那天下了场雪(上海难得11月下雪),然后在雪中踢球,可能是这学期玩的最开心的一次。</p><p><img src="https://s2.loli.net/2023/01/28/qfAOp3aRTZCK6HD.jpg" alt="拍的不好,照片里一片雪都看不见"></p><p> 接下来就是很魔幻的第二周,首先是因为混管阳性,东17封楼了。然后在短暂的经历了一天不能堂食的生活后,我校再次回到了线上课的时代,好在打饭还是允许的,楼也没封。目前(2022.12.9)<strong>情况未知</strong>,貌似是鼓励返乡线上学习的(效仿之前清北的先例?),本来四级要求的做的核酸也不需要了,体测什么也取消了,总之非常的神奇非常的魔幻,我也不太敢评价。</p><p><img src="https://s2.loli.net/2023/01/28/1zCl2wjtZmPFdVM.jpg" alt="群里的通知"></p><p> 本人最终于12月18日回家了,然而回家前就已经感觉不太好了,回家不到一周果然阳了。阳了感觉非常不好,高烧了三天,将近一周的课都没听,大作业也一点没动,再加上期末考临近,压力巨大。不过好在后续恢复非常快,而且没有很剧烈的咳嗽等后遗症影响,状态恢复的非常好,并没有太多的影响期末考试。</p><p> 月末,好多课程作业的ddl接踵而至,确实有点忙,幸好阳之前已经基本完成数分论文和伟大思想论文(顺便打个广告:<a href="https://github.com/DarkSharpness/DarkSharpness/blob/main/Tex/MA/MA.pdf">数分</a>,<a href="https://github.com/DarkSharpness/DarkSharpness/blob/main/Tex/Quantum/quantum.pdf">伟大</a>)。感谢几位好友的帮助,我能在年前基本完成这些作业。</p><h1 id="1月"><a href="#1月" class="headerlink" title="1月"></a>1月</h1><p> 考试月,事情不多,主要是在复习。对我来说,我只care数分、线代、英语和程设。在这四门中,首先考的是数学分析,这张卷子说实话感觉不太难,但是问题在于我这段时间基本没咋做数分题目,导致做的太慢了,结果2个小时最后两题(含一道附加题)都没看到,考完真的急死了,毕竟理论最高分也就只有90了,更何况前面还没有检查。结果不出所料的裂开了,而且数分卡在了94分这个尴尬的分数,不过确实下半学期数分上的精力花的少,罪有应得。第二个考的好像是英语,英语说实话我也没指望能考多高,前段时间因为新冠导致好长一段时间没听英语听力了,听力也不出意外的出了些意外;阅读选择倒是不难,毕竟我可是大城市做题家(除了应试啥都不会的废物罢了);作文中规中矩,反正还是和以前一样,即兴发挥,啥句子都不准备,完全靠感觉,然后凑一些老师喜欢听的观点升华一下,大概就能拿高分。最后在10号考了剩下两门,程序设计完全没复习,反正摆就完事了,C++不会的特性太多了,大概扫了一遍书就去考了。这里不得不吐槽下线性代数,考试卷子实在太简单了,而作为一个因为新冠从12月中旬就没听过线代课、作业全靠糊、上课狂发呆的普通学生,我居然都能提前一个小时做完而且没检查出错就能拿100,实在是楽坏了。然后上海下大雪了两天,有点怀念前面12月初那次雪天踢球了qwq。</p><p><img src="https://s2.loli.net/2023/01/28/sKcqAfyiVUI1uNB.jpg" alt=""></p><p> 考完以后基本放飞自我,直接开摆,每天睡眠高达10小时,作息非常健康。除了有几天去走亲访友,基本上每天我都呆在房间里看电脑,当然除了玩minecraft(这是主线!),我还在写一些 C++ 模板以及研究modern C++ 特性。作为一个 CSP-S 压线一等奖的蒟蒻,我觉得算法已经来不及补课,计划等春节后再去补习。当然考完试,我也终于有时间把以前没好好听过的和新专辑都刷几遍的,比如 IOSYS 那几张 jrock 专辑(墙裂推荐,附 <a href="https://music.163.com/playlist?id=8063965041&userid=3215760601">网易云歌单</a>) 以及 Demetori 的新专辑 《寂光寂滅 ~ The Truth of the Cessation of Dukkha》,还有可爱的ayo、merami的新歌。说实话,感觉现在车万同人音乐已经基本快要走向其终点了,C101 会场摊位数占比再次破新低,回到了2010年以前东方还未到最火的时候的状况了。感觉很难过,不过好像我也做不了什么,很欣慰还有几个老社团依然在坚持。</p><p><img src="https://s2.loli.net/2023/01/28/vM5PFD8aEqmjVnS.png" alt="我在minecraft里的前卫小屋(雾)"></p><p><img src="https://s2.loli.net/2023/01/22/nl4iJLGsUtzafKx.png" alt=""><br><img src="https://s2.loli.net/2023/01/22/bo9uWGMsqkgvirE.png" alt=""><br><img src="https://s2.loli.net/2023/01/22/HSQ8ZzOx7LeN9dB.png" alt=""><br><img src="https://s2.loli.net/2023/01/28/a9Pcuo3DweG7EZv.png" alt=""><br><img src="https://s2.loli.net/2023/01/28/9XzaFMyonV5rctB.png" alt="东方Gartic服务器(QQ:329193219)的春节烟火会"></p><!-- 图片 --><h1 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h1><p> 这学期,我觉得我收获了许多。首先是学子讲坛,这极大的扩展了我的视野,这样的形式让我接触到了好多我不可能接触到的知识。然后是数学方面,我学习了部分入门级别的数学分析的知识,还有简单的线性代数知识,其中特别感谢 <a href="http://basics.sjtu.edu.cn/~chen/teaching/">Yijia Chen</a> 一学期的教导,其讲解线性代数十分细致,而且其也很好的诠释了线性代数中许多抽象公式背后的几何直观。这样的讲解方法,我非常喜欢,而且其slides详细到即使我不听课只看讲义,我也能很快理解其所想要表达的东西。还有就是程序设计,我学到了许多 C++ 的语法基础,这填补了我的知识体系漏洞,而那些大小作业不仅巩固了我破烂的算法基础,还提升了我的工程能力。感谢可爱的翁老师以及耐心的助教们!在课外,我学了一点点音频处理基础以及 Modern C++ 的特性,为了写这个破blog还学了不少 CSS 和 JS 的知识(然而目前主要还是抄板子或者查百度)。</p><p> 最后的最后,祝您新年快乐,感谢您耐心看到这里!</p>]]></content>
<summary type="html">一个菜狗瞎写的一点也不EMO的文案</summary>
<category term="随笔" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/"/>
<category term="总结" scheme="http://darksharpness.github.io/categories/%E9%9A%8F%E7%AC%94/%E6%80%BB%E7%BB%93/"/>
<category term="随笔" scheme="http://darksharpness.github.io/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
</feed>