-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
815 lines (759 loc) · 58.1 KB
/
atom.xml
File metadata and controls
815 lines (759 loc) · 58.1 KB
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
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://yang2096.github.io</id>
<title>YANG's Blog</title>
<updated>2023-03-18T06:55:01.461Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://yang2096.github.io"/>
<link rel="self" href="https://yang2096.github.io/atom.xml"/>
<subtitle>Yet ANother Good blog</subtitle>
<logo>https://yang2096.github.io/images/avatar.png</logo>
<icon>https://yang2096.github.io/favicon.ico</icon>
<rights>All rights reserved 2023, YANG's Blog</rights>
<entry>
<title type="html"><![CDATA[[rust初见] 一个关于生命周期的问题]]></title>
<id>https://yang2096.github.io/post/rust-chu-jian-yi-ge-guan-yu-sheng-ming-zhou-qi-de-wen-ti/</id>
<link href="https://yang2096.github.io/post/rust-chu-jian-yi-ge-guan-yu-sheng-ming-zhou-qi-de-wen-ti/">
</link>
<updated>2023-03-18T06:16:18.000Z</updated>
<content type="html"><![CDATA[<blockquote>
<p>这个问题来自 <a href="https://practice.rs/lifetime/advance.html#a-difficult-exercise">practice.rs</a></p>
</blockquote>
<h1 id="一个问题">一个问题</h1>
<p>两行注释是编译器给出的报错</p>
<pre><code class="language-rust">/* 使下面代码正常运行 */
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop(); // mutable borrow occurs here
println!("Interface should be dropped here and the borrow released");
use_list(&list); // cannot borrow `list` as immutable because it is also borrowed as mutable
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
</code></pre>
<h1 id="为什么可变引用的生命周期会持续到最后一行">为什么可变引用的生命周期会持续到最后一行?</h1>
<ol>
<li>对于对象的引用 的生命周期是 小于等于 构成该对象的引用类型的字段 的生命周期的.</li>
<li><code>self</code> 本身的生命周期只需要大于等于 <code>get_interface</code> 内部 + <code>Interface.manager</code> 两者中更长的那一个生命周期.</li>
<li>但是 <code>get_interface</code> 函数中 <code>self</code> 的生命周期被标注为与 <code>List::manager::text</code> 相同的生命周期.</li>
<li>再加上后续 <code>main</code> 中还有对 <code>list</code> 的引用, 所以 <code>self</code> 的生命周期相当于被不合适地标注得更长了, 导致与后续的引用冲突.
<ol>
<li>这么说合适吗? 毕竟标注不会延长引用的生命周期</li>
<li>但是生命周期注明的 "规则" 会被编译器应用, 用于在无法推断生命周期之间的关系时去使用, 最终检查对象的生命周期是否与使用的实际情况符合.</li>
</ol>
</li>
</ol>
<h1 id="如何改正">如何改正?</h1>
<ol>
<li>首先需要收敛 <code>self</code> 的生命周期, 给它一个和底层 <code>text &'a str</code> 不同的生命周期.</li>
<li>又由于 <code>List</code> 中的 <code>Manager<'a></code> 不是引用, 所以 <code>List::manager</code> 生命周期和 <code>List</code> 对象相同. 所以给 <code>self</code> 与 <code>text</code> 不同的生命周期会导致<code>List::manager</code> 的生命周期与 <code>List::manager::text</code> 的不同.</li>
<li>但是 <code>Interface</code> 的定义中, <code>Interface::manager</code> 与 <code>Interface::manager::text</code> 被标注成相同的生命周期, 所以需要进一步改变这两者的生命周期关系, 将其分离.</li>
</ol>
<h1 id="最终的效果">最终的效果</h1>
<pre><code class="language-rust">struct Interface<'a, 'b> {
manager: &'a mut Manager<'b>
}
impl Interface<'_, '_> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b,'a> {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let text = "hello";
let mut list = List {
manager: Manager {
text
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
</code></pre>
<h1 id="总结">总结</h1>
<ol>
<li>对于结构体本身的引用 与 其底层引用类型的字段 两者的生命周期是不同的, 前者肯定小于等于后者, 而且一般使用的时候是小于的.</li>
<li>结构体中有引用字段的嵌套, 或者多个同级的引用字段, 需要考虑他们之间的生命周期关系.
<ol>
<li>对于上面的代码, <code>Interface<'a, 'b></code> 中 <code>'a</code> <code>'b</code> 的关系没有注明, 但其实是有隐式的 <code>'b : 'a</code> 存在的.</li>
<li>如果颠倒过来, 就会导致没修改前相同的错误, 本质上还是让 self 的生命周期变大了.</li>
</ol>
</li>
</ol>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[读《领域驱动设计》]]></title>
<id>https://yang2096.github.io/post/domain_driven_design/</id>
<link href="https://yang2096.github.io/post/domain_driven_design/">
</link>
<updated>2021-12-06T13:57:26.000Z</updated>
<summary type="html"><![CDATA[<p>领域是由业务上知识和需求构成的,领域驱动设计就是在强调模型要反应领域知识,而设计实现要与模型密切关联,相辅相成。</p>
]]></summary>
<content type="html"><![CDATA[<p>领域是由业务上知识和需求构成的,领域驱动设计就是在强调模型要反应领域知识,而设计实现要与模型密切关联,相辅相成。</p>
<!-- more -->
<p>我对业务建模的态度与书中上述观点有些相似之处,不过我之前只有一些模糊的认识:一、设计与业务现实背离肯定是有害的。二、如果在努力之后,模型还是不能贴合现实世界,那一定是对现实世界的认识还不够深刻。</p>
<p>第二点确实还停留在一个浅层的定论上,而按书中所言,模型不能反应领域的时候,一方面是需要继续学习领域知识,这里又可以分成直接学习相关知识或间接学习别人的建模方式;一方面也可以在模型内部寻找突破,比如挖掘遗漏的概念、分析现有模型矛盾之处等。</p>
<p>这只是本书带来的深层认知的一个小的点。</p>
<p>书里还有一些让人顿悟的亮点,比如:</p>
<ul>
<li>图不是越详细越好,如果想在图中展现出所有的细节,那么图就会沦为另一份代码。</li>
<li>语言有魔力。人类的自然语言本身就包含了高度的抽象能力。开发人员之间的日常交流中蕴含着遗漏的概念、突破的契机。而当一个抽象概念的恰当名字诞生时,整个开发组、架构组以及业务方都会低语它,并将其纳入 Ubiquitous Language 当中。</li>
<li>我原以为“封装变化”已经很高级了,但往上还有“封装认知”这一说法。屏蔽变化只是减少认知负载的一种。让接口表现其意图,让人没有心理负担地去使用,这样的封装确实更值得实现。</li>
<li>代码的复用是好的,但模型的复用更值得追求。(“分析模式”可以说是大份的泡面里的脱水蔬菜了)</li>
<li>类之间的关系有“低耦合,高内聚”的说法,这其实是概念上的关系,在设计上的,冰山露出水面的一角。</li>
<li>没有参与过开发的架构师提出的模型或建议可以忽略。模型之于设计,架构师之于开发组。模型与设计上的相辅相成,离不开架构与开发的密切反馈。</li>
</ul>
<p>本书阐述的观点、模式,是比平常的面向对象设计更高一层的设计理念、元素。面向对象的设计关注得更多的是类本身的职责以及类与类之间的关系。在一个大型的系统中,过于关注具体的类,容易陷入“只见树木,不见森林”的陷阱中。所以大型系统中需要关注的是更大尺度上的结构,也就是书中最后一部分“战略设计”中才提到的 Bounded Context 以及“大型结构”(模型的设计模式)。</p>
<p>这一部分十分重要,但也是书中限于篇幅无法详尽阐述的,具体而言就是论证和结论固然是好的,但举的例子无法起到表达观点的作用。毕竟尺度拉大了,微观之处就会失去聚焦。例子中虽然有结构上的叙述,但没法让人看到形成这种结构的原因——即省略的细节。</p>
<p>书中还给读者描绘了一幅程序员梦寐以求的理想图卷:经过精炼和重构之后,项目获得了突破,深层模型与柔性设计浮出水面。深层模型中各种概念构成了一种语言,通过简洁的组合就可以准确地描述业务。同时柔性设计使得代码易于理解,方便修改甚至是重构。</p>
<p>说实话,这样的程序太美好了,说不定每一个不写垃圾代码的程序员升入天堂后会有72个这样的项目供其开发维护(🌿,死了还要写代码,这是天堂还是地狱)。这样的代码是值得追求的,不论是追求的过程还是最后达成的效果,都是一次洗礼,让人有一种明心见性的感悟(我没经历过,我猜的😅)。</p>
<p>好书,值得再读一遍,希望能在后续的工作中运用起来其中的原则。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[读《重构》]]></title>
<id>https://yang2096.github.io/post/refactoring/</id>
<link href="https://yang2096.github.io/post/refactoring/">
</link>
<updated>2021-09-21T14:43:03.000Z</updated>
<summary type="html"><![CDATA[<p>总以为经过这么多年大浪淘沙后能留下来的都是一些不会囿于时代的经典,但是在我看来这本书只能说明再经典的著作也有其历史的局限性。</p>
]]></summary>
<content type="html"><![CDATA[<p>总以为经过这么多年大浪淘沙后能留下来的都是一些不会囿于时代的经典,但是在我看来这本书只能说明再经典的著作也有其历史的局限性。</p>
<!-- more -->
<p>在书中占了很大比例的具体“重构”手法,在面向对象编程技术野蛮生长的初期也许是举足轻重的,它们的重要性和具体做法值得反复提出、重点科普。不过在站在2021年的今天来看,很多手法已经算是深入人心,习以为常了,或许这正是此书所完成的使命吧。</p>
<p>先把丑话说在前头,说说我看书的遗憾。</p>
<p>首先是内容结构上的。书中列举的重构手法,很多是成对出现的,比如函数下移&函数上移,以继承取代委托&以委托取代继承等等。虽然能明白具体问题需要具体分析,但是这种前后相反的,或者说共轭的手法经常出现,让我产生出一种“正话反话都让你说完了”的无可奈何之感。或许作者应该直接将成对出现的手法合在一起阐述,说明为何两者都有其存在的意义,更多地着墨于其解决的问题,而非具体的代码编写步骤。毕竟书中的许多手法已经在现代的编辑器中点两下鼠标即可实现。</p>
<p>太多的文字被用于叙述各种具体的手法(从第六章到第十二章),剩下的部分,第三章里的“Code Smell”值得一读。“Code Smell”是一些根本性问题的具体表现,如果作者直接说出这些问题,比如“不能耦合过高”、“应该分离关注点”这些高度凝练的原则,效果可能并不会太好。间接地指明问题的“体表特征”也许是更符合全书整体风格和实际工程实践的合适做法。还有第二章的重构原则,感觉多数已是“稀松平常的世界本来秩序的一部分”。</p>
<p>另一个遗憾之处是仰慕所带来的期待。在没有学习设计模式前,我对书中的这些手法也并非全然无知——可能还是这书潜移默化的功劳,所以在没读之前,我以为这本书说的是比代码组织高一层次的“重构”,会更多地关注于业务对代码构建的影响,比如会从需求分析开始,先谈谈如何归类这些需求,如何将层层叠叠的旧代码所应付的、业务一路演化而带来的需求化繁为简。中间再说说如何改造旧代码,就像书中实际有的。最后再探讨一下如何让系统能高度扩展、如何用优秀的架构设计抵抗代码腐化。好吧,是我想太多了。</p>
<p>当我觉得这书不尽人意的时候,也会想:究竟是这书不过如此,还是我未能体会其中深意呢?</p>
<p>但是在这本书出版的四年前,1995年,《设计模式》出版了。我觉得《设计模式》这样一本里程碑式的著作,背后代表的应该是一个高度发展的面向对象编程技术世界。有点难以想象《重构》的具体创作背景是什么,既然已经能总结出各类模式,为何还需要专门写一本书来总结这些细则呢?难道前人总结的设计模式是空中楼阁?难道软件工程业界也是理论领先于实践?</p>
<p>总的来说,这本书的“密度”算是比较低的,即使考虑排版比较宽松的因素,一天能看100页,也还是可以说明中间的重构手法实在是没有什么亮点可言。可能作者也有注意到这点,所以一开篇就说了:<br>
<img src="https://yang2096.github.io/post-images/1632416716018.png" alt="" loading="lazy"></p>
<p>连译者也说:<br>
<img src="https://yang2096.github.io/post-images/1632417989176.jpg" alt="" loading="lazy"></p>
<p>就我的情况来说,这书看了收获没有想象中的大。或许是我读书太功利了吧,恨不得看一本书就得涨一大截经验,连升好几级🤡。虽然吐槽了这么多,但还是不敢轻视书里写的那些手法,毕竟说不定哪天自己又写出了更值得吐槽的垃圾代码。到时候被别人用这些手法来“鞭尸”,岂不是无地自容?</p>
<p>书还有很多要看,代码还有很多要写,还能怎么办呢?一本本看,一行行写吧。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[读《设计模式》]]></title>
<id>https://yang2096.github.io/post/design_patterns/</id>
<link href="https://yang2096.github.io/post/design_patterns/">
</link>
<updated>2021-09-06T16:46:08.000Z</updated>
<content type="html"><![CDATA[<p>在买这本书之前看过很多网络上介绍设计模式的文章,一般会流于两种形式:一是给你举一些自以为贴切的例子,除了分散注意力外对模式的阐述毫无裨益。二是只谈模式本身的结构,对其作用的问题及上下文少有提及。我几次下定决心想要学完都没成功,一度以为自己心浮气躁,不能沉下心来夯实基础。☹️</p>
<p>直到我看了这本《设计模式 可复用面向对象软件的基础》的前两章。</p>
<p>就我的粗浅水平来看,第一章可能还算“平平无奇”,但是第二章是着实把我震住了。第二章讲的是一个文档编辑器的例子,短短三十页里面提及了八种模式,每一个模式的运用都非常自然,仿佛在面临这种场景之时,天生就应该使用这种设计模式。让人击节赞叹,让人念头通达,让人看到这种解决方式之后再也无法承认还有更妙的解法。好像一边看一边能听到作者就在旁边说:“老夫今天便是要教教你,什么叫编程之美!”</p>
<p>当然这样的说法可能是我有所夸张。这是在看到大海高山之后,相形见绌之下的喟叹。</p>
<p>虽然本人工作已经三年了,但是代码设计一直还停留在以面向过程的抽象为主的阶段。倒不是说面向过程的设计不好,但是我感觉:面向过程的程序,代码的复用只是以函数为单位,面对功能不断变化的迭代过程,确实力有未逮。因为函数之间的调用是最直接的,耦合十分紧密,一个改动往往需要牵连好几个函数。而面向对象的代码设计不仅带来了抽象建模上的改变,同时也增添了代码复用、结构设计上的优势。</p>
<p>以前我对面向对象的三大特性:封装、继承和多态的理解十分粗浅。</p>
<p>对于封装,以为只要成员变量是私有的,不让类以外的代码自由访问就算封装。看完书我才理解到:封装的高级形式是封装变化,封装的目的和优势之一是隔离。封装是将类内部的数据隐藏掉,使得外部只需关注它的行为。比如 Iterator 模式,就是封装了集合类的内部结构,使得外部只需关注遍历操作。这一点和运行时多态结合起来,更是强大:接口封装了所有行为,而承载这些行为的实体还能替换,使得行为也可以在运行时变化 。增加接口的实现就是功能的扩展,而使用接口的代码则可完全复用。这不就是“对扩展开放,对修改关闭”吗?而接口和实现之间的连接则是通过继承实现,书中虽说倾向于使用组合而非继承,但这是从代码复用的角度来说的,实际实现上为了满足接口的定义,继承也是很常见的。</p>
<p>如书所言,设计模式是针对一类问题,在特定上下文中,利用面向对象特性的一种解决方案。书中不断强调其针对的问题,一方面是强调设计模式的使用不能脱离具体领域,一种设计模式并不是面对一切问题的万能钥匙。二是让读者在遇到相似情况时能想到使用设计模式。我觉得后者是运用设计模式最难的部分,毕竟判断出该使用哪种模式之后,剩下的便是索然无味的代码实现。而能分析问题,站在更高的角度来观察整体的代码结构而非只看到实现,无疑对工程师的抽象能力、分析能力有着更高的要求。</p>
<p>整本书不愧是一代开山怪合著的精华,硬要挑一点毛病就只能说成书时间实在太早了,书中大量例子所使用的 SmallTalk 语言,别说看懂语法了,在此之前我连名字都没听说过。好在 C++ 还是能看懂的,不过就连 C++ 也是 Lambda 函数都不存在的版本。其实语言版本古老也还好,揭示了设计模式的通用性,只不过某些语言下确实更容易实现而已。</p>
<p>如果还有什么毛病也只能是我的,这是我第一次在拼多多上买书,结果前50页重复了,这就是拼多多的加量还降价吗?😂再也不在拼多多买书了。</p>
<p>最后还是对每种设计模式来几句自己的总结吧。</p>
<p><strong>Abstract Factory</strong>:需要创建一系列相关联的对象时使用,正如一个工厂里的商品都有同样的商标(内在的联系)。<br>
<strong>Builder</strong>:创建复杂的、带有可选项的对象时使用。有一些链式的函数调用最后一个<code>Build()</code>,就是这种模式。<br>
<strong>Factory Method</strong>:将创建和实现分离,创建使用者自己都不知道该用的类的时候使用。<br>
<strong>Prototype</strong>:创建的对象都可以从一个原始基础上略作改动时可以偷懒,先定义好原型,后面只用复制+少许改变就行。<br>
<strong>Singleton</strong>:会引入全局的状态,用着爽,但是不到必要情况还是别用了。</p>
<p><strong>Adapter</strong>:包装一个接口使其符合另一个接口的定义,是针对两个已有的接口在实现阶段而非设计阶段做出的补全。书里的 Pluggable Adapter 的意思是能运行时动态改变 Adaptee 的适配器,根据最少知识原则,Adapter 对 Adaptee 的假设越少,复用程度就能越大。Pluggable Adapter 通过注入行为而达到对 Adaptee 的最小假设。<br>
<strong>Bridge</strong>:在两组独立扩展的类层次之间做出的桥接。重点在于让两种类之间能独立地进行扩展,而非像 Adapter 一样去拟合两个类。两种类的接口甚至可以毫无关系,Implementer 只需提供给 Abstraction 所需要的操作即可。<br>
<strong>Composite</strong>:个人感觉最妙的模式,局部和整体有着统一的优美,可以嵌套的结构如同幂集一般构造出纷繁的形式。不区分组合和单个对象是设计的关键。常见于 UI 中各种组件的层次结构构造,文件目录的实现。<br>
<strong>Decorator</strong>:常与 Composite 联用,装饰前后的接口保持一致,适合简单地在原有行为前后加入新的操作。<br>
<strong>Facade</strong>:给多个子系统整合出一个聚合的入口。<br>
<strong>Flyweight</strong>:关键在于区分内部状态和外部状态。外部状态最好能通过计算得到,不然为了保存外部状态用到太多对象的话,就失去了使用享元模式的本意:通过抽离外部状态来使得大量细粒度的对象能用少数只有内部状态不同的对象复用来代替。书中对连续文本格式的处理使用了 BTree,也是不可多得的巧妙实现,算法与工程的自然结合。<br>
<strong>Proxy</strong>:同样是在对象外层套了一个壳,与 Decorator 的区别是关注于对内部对象的控制而非增加内部对象的功能。主要的用途是权限控制或延迟访问。最常见的实现可能是 C++ 里的智能指针。</p>
<p><strong>Chain of Responsibility</strong>:去除了请求的发起者和接收者之间的耦合,发起者不明确最后会由谁来处理请求。稍加改造可以做成链上的所有人都处理请求的形式。没有相关实践,感受不深。<br>
<strong>Command</strong>:将请求封装为对象,接收者是该对象的 成员变量/参数,在命令执行(调用 command 对象的 Execute 方法)时被 command 对象使用。优势在于能记录历史并实现动作的撤销。<br>
<strong>Interpreter</strong>:和 Composite 模式有点类似,具体实现的结构需要对应文法的范式。“解释”只是对语法树整体操作的一个指代,实际遍历树节点的任何操作都可以是一种“解释”。但是语法树的构建不归它管,缺点也很明显:修改文法需要同步修改类实现。<br>
<strong>Iterator</strong>:目的是隐藏集合内部的具体结构。外部迭代器由使用者控制步进,内部迭代器由使用者传入操作,Go 中的 <code>sync.Map:Range</code> 就是一种内部迭代器。<br>
<strong>Mediator</strong>:多个组件不直接交互,降低相互间的耦合,由中介者来接收消息、通知组件。一个 “manager”类控制多个组件的行为也许可以算作中介者模式,这样的话也许就使用得太自然以至于少有人意识到是这种模式,<br>
<strong>Memento</strong>:将状态保存到外部,必要时再取回来。实现上需要控制好访问的权限,保证只由 Memento 能访问 Originator 的内部状态,同时也只有 Originator 能从 Memento 中取出状态。具体场景没经历过,感受不深。<br>
<strong>Observer</strong>:感觉名字起得不是太贴合,只体现了整个流程的后半段:Observer 从 Subject 查看状态,前面的 Subject 通知 Observer 没有在名字中体现。“通知/订阅”模式又稍显繁复,想不出更好的名字。何时通知、何样的消息需要通知也是实现上需要关注的点。<br>
<strong>State</strong>:用对象的替换代表状态的改变;用方法的运行时多态,实现不同状态下的不同行为。状态对象本身可以是“无状态”的,只实现行为,状态在上下文中传入。我最早实现的一个模式,对大量 <code>switch case</code> 语句的优化效果显著,属于对面向对象特性的精妙运用。<br>
<strong>Strategy</strong>:用对象封装算法。在函数式编程中可能属于基本操作。<br>
<strong>Template Method</strong>:使用抽象接口定义了一系列操作的执行逻辑,聚合成一个方法,即某一操作的模板。后面只需要实现&替换抽象接口就算是“模板实例化”。也算是面向对象程序设计的常见操作。<br>
<strong>Visitor</strong>:Visitor 需要实现对 ObjectStructure 中各类 Element 的 <code>visit</code> 操作(使用函数重载,用参数区分操作可能更简洁)。将识别类型的位置放到具体类型的 Element 中是其巧妙构思所在,是对书中介绍的“double-dispatch 双分派”的一种模拟,最终调用的操作取决于两个接收者的类型(Visitor 和 Element)和具体操作的类型(<code>visit</code>)。</p>
<p>下一个阅读目标是《重构:改善既有代码的设计》,重构以达成设计模式,先明确了目标(设计模式)再学习达成的方法(重构)。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[C++对象模型实例分析]]></title>
<id>https://yang2096.github.io/post/C++-object-model-1/</id>
<link href="https://yang2096.github.io/post/C++-object-model-1/">
</link>
<updated>2020-06-13T14:45:40.000Z</updated>
<content type="html"><![CDATA[<h2 id="c-多态的底层支持">C++ 多态的底层支持</h2>
<p>C++ 中使用非指针,非引用的对象来调用函数时,在编译阶段其调用的函数地址就已经确定下来,没有(也不需要)动态绑定的效果。即使是多个重名函数,也可以经过函数匹配确定为其中一个,即静态多态。</p>
<p>此时对象的数据和函数其实是”分裂“的,对象所在的内存当中没有关于类型以及所属函数的信息。</p>
<p>通过父类的指针或引用来调用虚函数,能调用到子类对象实际上实现的函数,即子类中重写的虚函数。这就是动态多态。而要实现延迟绑定,需要类的类型信息与对象的数据”同步“存在。即可以通过检查对象数据中的某个字段,得到整块内存的类型信息。</p>
<p>因此 C++ 中引入了虚函数表来指明对象的类型、关联所属的函数。通过在对象中放置虚函数表指针 <code>vptr</code> 来关联内存与类型。在运行时通过读取虚表中的内容,来实现多态性。</p>
<p>本文通过分析一些简单的例子,尝试解释这一整套流程。</p>
<h2 id="虚函数表的基本结构">虚函数表的基本结构</h2>
<h3 id="最基础的情况">最基础的情况</h3>
<p>最基础的情况就是单继承,没有虚继承的情况。</p>
<pre><code class="language-c++">#include <iostream>
using namespace std;
class B
{
public:
int ib;
char cb;
B() : ib(0xBB), cb('A') {}
virtual void f() { ++ib; cout << "B::f()" << endl; }
virtual void Bf() { --ib; cout << "B::Bf()" << endl; }
};
class D : public B
{
public:
int id;
char cd;
D() : id(0xDD), cd('D') {}
virtual void f() { ++cb; cout << "D::f()" << endl; }
virtual void Df() { --cb; cout << "D::Df()" << endl; }
};
int main()
{
D d;
B * b_ptr = &d;
d.Bf();
b_ptr->Bf();
}
</code></pre>
<p>在 g++ 中使用 <code>-fdump-class-hierarchy</code> 或者 <code>-fdump-lang-class</code> 选项,又或者在 VisualStudio 的 <code>项目 -> 属性 -> 配置属性 -> C/C++ -> 命令行 -> 其他选项</code> 中添加选项 <code>/d1reportAllClassLayout</code> 可以导出对象的虚函数表结构。</p>
<p>这里我用的 g++ 版本是 gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) ,通过<br>
<code>g++ -g -o single ./single_hierarchy.cpp -fdump-class-hierarchy</code><br>
得到上述两个类的虚函数表和对象内存布局,再经过 <code>c++filt</code> 进行 <code>demangle</code> 得到易于阅读的符号名称。</p>
<h4 id="虚表布局">虚表布局</h4>
<pre><code>Vtable for B
B::vtable for B: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for B)
16 (int (*)(...))B::f
24 (int (*)(...))B::Bf
Class B
size=16 align=8
base size=13 base align=8
B (0x0x7f3d6ce36720) 0
vptr=((& B::vtable for B) + 16)
Vtable for D
D::vtable for D: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for D)
16 (int (*)(...))D::f
24 (int (*)(...))B::Bf
32 (int (*)(...))D::Df
Class D
size=24 align=8
base size=21 base align=8
D (0x0x7f3d6ce6f958) 0
vptr=((& D::vtable for D) + 16)
B (0x0x7f3d6ce76660) 0
primary-for D (0x0x7f3d6ce6f958)
</code></pre>
<p>再结合<code>gdb</code>来查看实际程序中的内存布局:</p>
<pre><code class="language-c++">(gdb) info locals
d = {<B> = {_vptr.B = 0x8201d28 <vtable for D+16>, ib = 187, cb = 65 'A'}, id = 221, cd = 68 'D'}
b_ptr = 0x7ffffffee180
(gdb) x/3xg &d
0x7ffffffee180: 0x0000000008201d28 0x00000041000000b9
0x7ffffffee190: 0x00007f44000000dd
(gdb) x/5xg 0x8201d18
0x8201d18 <_ZTV1D>: 0x0000000000000000 0x0000000008201d60
0x8201d28 <_ZTV1D+16>: 0x0000000008000caa 0x0000000008000c26
0x8201d38 <_ZTV1D+32>: 0x0000000008000cf2
</code></pre>
<p>这里查看对象 d 的内存布局 <code>x/3xg &d</code> 中的 3 是从类 D 的大小(24/8)得到。<br>
查看虚函数表的布局 <code>x/5xg 0x8201d18</code> 中的 5 是从 <code>D::vtable for D: 5 entries</code> 这里看到有 5 个表项, <code>0x8201d18</code> 则是 <code>_vptr.B = 0x8201d28</code> 减去偏移 16 得到。</p>
<p>通过 <code>nm -Cn ./single</code> 得到符号的地址信息</p>
<pre><code>// 仅展示相关部分
...
0000000000000bb2 W B::B()
0000000000000bb2 W B::B()
0000000000000bde W B::f()
0000000000000c26 W B::Bf()
0000000000000c6e W D::D()
0000000000000c6e W D::D()
0000000000000caa W D::f()
0000000000000cf2 W D::Df()
...
0000000000201d18 V vtable for D
0000000000201d40 V vtable for B
0000000000201d60 V typeinfo for D
0000000000201d78 V typeinfo for B
...
</code></pre>
<p>可以看到对象 d 中起始位置存储的是 <code>vptr</code>,指向了类 D 的 vtable + 16 <code>0x0000000008201d28</code>。<br>
这里与 <code>nm</code> 命令导出的地址信息相差了 <code>0x8000000</code>,我想应该是 <code>nm</code> 中给出的地址是段内偏移,而 <code>.text</code> 段的开始地址是<code>0x8000000</code>,所以需要加上这么多来得到运行时的绝对地址。可以查看进程的 <code>mmap</code> 信息确认。</p>
<p>继续查看类 D 的 vtable,发现其中存储的信息确实与<code>g++</code>导出的布局一一对应。<br>
这里可以看出对象内部的 <code>vptr</code> 并不是指向虚函数表的起始位置,而是越过了开头的两项。<br>
第二项从名字上就可以看出指向的是类的类型信息。那么第一项是什么呢?从这个简单的样例还看不出所以然来,不过从其他资料可知,虚函数表的第一项是用来指明继承体系中不同类型的指针指向当前对象实例时,<code>this</code> 指针与对象的起始位置之间的偏移。一个派生类通过合法的向上转型成不同的父类或祖类指针后,这些指针指向的其实是对象实例的不同部分。</p>
<h4 id="虚表指针初试锋芒">虚表指针初试锋芒</h4>
<p>将上述代码输入到<a href="https://godbolt.org/">这个网站</a>可以观察到编译后的汇编代码。<br>
<img src="https://yang2096.github.io/post-images/1629730271727.png" alt="" loading="lazy"><br>
<code>d.Bf();</code> 这条语句是使用对象直接调用虚函数,在汇编代码的第 123 行可以看出在编译期就确定了是直接调用函数 <code>B::Bf()</code>。而后一条语句<code>b_ptr->Bf();</code>通过指针来调用虚函数时,则多出了几步:</p>
<pre><code class="language-asm">mov rax, QWORD PTR [rbp-8] // b_ptr 的数值
mov rax, QWORD PTR [rax] // 获取虚函数表的地址(跳过前两项)
add rax, 8 // 第二个函数表项的地址(实际是第四项)
mov rax, QWORD PTR [rax] // 虚函数 B::Bf() 的地址
mov rdx, QWORD PTR [rbp-8] // this 作为参数
mov rdi, rdx
call rax // 调用 B::Bf()
</code></pre>
<p>这就是虚表指针发挥作用的时刻之一,动态绑定所运行的函数。</p>
<h3 id="多继承的情况">多继承的情况</h3>
<p>类 C 同时继承类 A 和类 B</p>
<pre><code class="language-c++">#include <string>
#include <iostream>
using namespace std;
class A {
public:
int i1;
char c1;
A() : i1(0xAA), c1('A') {}
virtual void f() { cout << "A::f()" << endl; }
virtual void f1() { cout << "A::f1()" << endl; }
};
class B {
public:
int i2;
char c2;
B() : i2(0xBB), c2('B') {}
virtual void f() { cout << "B::f()" << endl; }
virtual void f2() { cout << "B::f2()" << endl; }
};
class C : public B, public A
{
public:
int i3;
char c3;
C() : i3(0xCC), c3('C') {}
virtual void f() { cout << "C::f()" << endl; }
virtual void f2() { cout << "C::f2()" << endl; }
};
int main(void)
{
C c;
C * c_ptr = &c;
B * b_ptr = &c;
A * a_ptr = &c;
cout << "c_ptr : " << c_ptr << endl
<< "b_ptr : " << b_ptr << endl
<< "a_ptr : " << a_ptr << endl;
long* data = (long *)* (long *)b_ptr;
cout << hex << data << endl;
typedef void(*Func)(void *);
Func fun = (Func) data[0];
fun(b_ptr);
}
</code></pre>
<h4 id="虚表布局-2">虚表布局</h4>
<pre><code class="language-c++">Vtable for A
A::vtable for A: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for A)
16 (int (*)(...))A::f
24 (int (*)(...))A::f1
Class A
size=16 align=8
base size=13 base align=8
A (0x0x7f188fae6720) 0
vptr=((& A::vtable for A) + 16)
Vtable for B
B::vtable for B: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for B)
16 (int (*)(...))B::f
24 (int (*)(...))B::f2
Class B
size=16 align=8
base size=13 base align=8
B (0x0x7f188fb25660) 0
vptr=((& B::vtable for B) + 16)
Vtable for C
C::vtable for C: 9 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for C)
16 (int (*)(...))C::f
24 (int (*)(...))C::f2
32 (int (*)(...))C::f3
40 (int (*)(...))-16
48 (int (*)(...))(& typeinfo for C)
56 (int (*)(...))C::non-virtual thunk to C::f()
64 (int (*)(...))A::f1
Class C
size=40 align=8
base size=37 base align=8
C (0x0x7fefa3b89d20) 0
vptr=((& C::vtable for C) + 16)
B (0x0x7fefa3b96900) 0
primary-for C (0x0x7fefa3b89d20)
A (0x0x7fefa3b96960) 16
vptr=((& C::vtable for C) + 56)
</code></pre>
<p>再接着看到 <code>gdb</code> 中读取出的实际运行期数据:</p>
<pre><code class="language-c++">// 运行到了最后一行
(gdb) info locals
c = {<B> = {_vptr.B = 0x8201ca0 <vtable for C+16>, i2 = 187, c2 = 66 'B'}, <A> = {_vptr.A = 0x8201cc8 <vtable for C+56>, i1 = 170, c1 = 65 'A'}, i3 = 204, c3 = 67 'C'}
c_ptr = 0x7ffffffeddf0
b_ptr = 0x7ffffffeddf0
a_ptr = 0x7ffffffede00
data = 0x8201ca0 <vtable for C+16>
fun = 0x800123d <__libc_csu_init+77>
(gdb) x/5xg &c
0x7ffffffeddf0: 0x0000000008201ca0 0x00000042000000bb // vtable for C+16 B::c2 B::i2
0x7ffffffede00: 0x0000000008201cc8 0x00000041000000aa // vtable for C+56 A::c1 A::i1
0x7ffffffede10: 0x00007f43000000cc // C::c3 C::i3
(gdb) x/9xg data-2
0x8201c90 <_ZTV1C>: 0x0000000000000000 0x0000000008201d18 // 0 & typeinfo for C
0x8201ca0 <_ZTV1C+16>: 0x0000000008001126 0x0000000008001174 // C::f C::f2
0x8201cb0 <_ZTV1C+32>: 0x00000000080011ac 0xfffffffffffffff0 // C::f3 -16
0x8201cc0 <_ZTV1C+48>: 0x0000000008201d18 0x000000000800116e // & typeinfo for C C::non-virtual thunk to C::f()
0x8201cd0 <_ZTV1C+64>: 0x0000000008000ff6 // A::f1
</code></pre>
<p>基本上可以看出,<code>c</code> 中有两个虚表指针 <code>vptr</code>, 分别"服务"于它的两个直接基类。向上转型到哪个类,其指针就指向基类实例中哪个 <code>vptr</code> 的位置。此例中的 <code>a_ptr</code> 就指向了对象<code>c</code>中第二个<code>vptr</code>的地址<code>0x7ffffffede00</code>。</p>
<p>之前分析了虚函数调用时的汇编代码,知道了调用虚函数是依靠<code>vptr</code>的偏移来定位的,也就是说基类中的虚函数和派生类中重写的虚函数应该是位于虚表中的同一位置。所以基类与派生类两者的虚函数表结构应该是可以”重叠“上的。</p>
<p>具体到当前这个例子,如果将基类和派生类的虚表这样放置:</p>
<pre><code>C::vtable for C: 9 entries // B 的虚表
0 (int (*)(...))0 0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for C) 8 (int (*)(...))(& typeinfo for B)
16 (int (*)(...))C::f 16 (int (*)(...))B::f
24 (int (*)(...))C::f2 24 (int (*)(...))B::f2
32 (int (*)(...))C::f3 // A 的虚表
40 (int (*)(...))-16 0 (int (*)(...))0
48 (int (*)(...))(& typeinfo for C) 8 (int (*)(...))(& typeinfo for A)
56 (int (*)(...))C::non-virtual thunk to C::f() 16 (int (*)(...))A::f
64 (int (*)(...))A::f1 24 (int (*)(...))A::f1
</code></pre>
<p>可以看出,除了派生类中新增的虚函数被追加到属于第一个基类的虚函数表尾部,左右其他部分的表项都是一一对应的。派生类中重写的虚函数“顶替”了基类中原来的表项,没有重写的则保持了原来的值。</p>
<p>如果继承的层次比较多的话,虚表的结构也是由上至下一层一层“继承”过来的。</p>
<p>至此,在不涉及到虚继承的情况下,虚表的结构以及对象的结构都算是比较易得的。单继承时虚表结构“父子”相承,多继承时无非多几个 <code>vptr</code>。即便是多重继承,从上至下细细推导也不难得出所有的虚表结构。</p>
<p>顺带一提虚表中的<code>C::non-virtual thunk to C::f()</code>的作用是当使用基类指针访问子类中重写的虚函数时,将传入的<code>this</code>指针减去偏移后,得到对象的首地址(派生类的<code>this</code>)再调用该虚函数。不过这里的偏移是编译期就能确定的一个常数,而不是使用<code>vptr - 2</code>位置上的偏移值。</p>
<h2 id="引入虚继承带来的变化">引入虚继承带来的变化</h2>
<p>虚继承一般常见于钻石型继承结构中。</p>
<pre><code class="language-c++">// 钻石型虚继承
class B
{
public:
int ib;
char cb;
public:
B() : ib(0xBB), cb('A') {}
virtual void f() { ++ib; cout << "B::f()" << endl; }
virtual void Bf() { --ib; cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
int ib1;
char cb1;
public:
B1() : ib1(0xB1), cb1('B') {}
virtual void f() { ++ib; cout << "B1::f()" << endl; }
virtual void f1() { --cb; cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : virtual public B
{
public:
int ib2;
char cb2;
public:
B2() : ib2(0xB2), cb2('C') {}
virtual void f() { ++ib; cout << "B2::f()" << endl; }
virtual void f2() { --ib; cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
char cd;
public:
D() : id(0xDD), cd('D') {}
virtual void f() { ++ib; cout << "D::f()" << endl; }
virtual void f1() { --ib; cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
int main()
{
D d;
B1 * b1_ptr = &d;
B2 * b2_ptr = &d;
B * b_ptr = &d;
cout << "b_ptr : " << b_ptr << endl
<< "b1_ptr : " << b1_ptr << endl
<< "b2_ptr : " << b2_ptr << endl
<< "d_ptr : " << &d << endl;
cout << typeid(*b1_ptr).name() << endl;
B1 b1;
b_ptr = &b1;
b_ptr->Bf();
b1_ptr = &b1;
b1_ptr->Bf();
}
</code></pre>
<p>依旧是先看这些类的布局:</p>
<pre><code>Vtable for B
B::vtable for B: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for B)
16 (int (*)(...))B::f
24 (int (*)(...))B::Bf
Class B
size=16 align=8
base size=13 base align=8
B (0x0x7ffad8f56720) 0
vptr=((& B::vtable for B) + 16)
Vtable for B1 Construction vtable for B1 (0x0x7ffad8f8fd00 instance) in D
B1::vtable for B1: 12 entries D::construction vtable for B1-in-D: 12 entries
0 16 0 40
8 (int (*)(...))0 8 (int (*)(...))0
16 (int (*)(...))(& typeinfo for B1) 16 (int (*)(...))(& typeinfo for B1)
24 (int (*)(...))B1::f 24 (int (*)(...))B1::f
32 (int (*)(...))B1::f1 32 (int (*)(...))B1::f1
40 (int (*)(...))B1::Bf1 40 (int (*)(...))B1::Bf1
48 0 48 0
56 18446744073709551600 56 18446744073709551576
64 (int (*)(...))-16 64 (int (*)(...))-40
72 (int (*)(...))(& typeinfo for B1) 72 (int (*)(...))(& typeinfo for B1)
80 (int (*)(...))B1::virtual thunk to B1::f() 80 (int (*)(...))B1::virtual thunk to B1::f()
88 (int (*)(...))B::Bf 88 (int (*)(...))B::Bf
VTT for B1
B1::VTT for B1: 2 entries
0 ((& B1::vtable for B1) + 24)
8 ((& B1::vtable for B1) + 80)
Class B1
size=32 align=8
base size=13 base align=8
B1 (0x0x7ffad8f8f958) 0
vptridx=0 vptr=((& B1::vtable for B1) + 24)
B (0x0x7ffad8f96660) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& B1::vtable for B1) + 80)
Vtable for B2 Construction vtable for B2 (0x0x7ffad8f8fd68 instance) in D
B2::vtable for B2: 12 entries D::construction vtable for B2-in-D: 12 entries
0 16 0 24
8 (int (*)(...))0 8 (int (*)(...))0
16 (int (*)(...))(& typeinfo for B2) 16 (int (*)(...))(& typeinfo for B2)
24 (int (*)(...))B2::f 24 (int (*)(...))B2::f
32 (int (*)(...))B2::f2 32 (int (*)(...))B2::f2
40 (int (*)(...))B2::Bf2 40 (int (*)(...))B2::Bf2
48 0 48 0
56 18446744073709551600 56 18446744073709551592
64 (int (*)(...))-16 64 (int (*)(...))-24
72 (int (*)(...))(& typeinfo for B2) 72 (int (*)(...))(& typeinfo for B2)
80 (int (*)(...))B2::virtual thunk to B2::f() 80 (int (*)(...))B2::virtual thunk to B2::f()
88 (int (*)(...))B::Bf 88 (int (*)(...))B::Bf
VTT for B2
B2::VTT for B2: 2 entries
0 ((& B2::vtable for B2) + 24)
8 ((& B2::vtable for B2) + 80)
Class B2
size=32 align=8
base size=13 base align=8
B2 (0x0x7ffad8f8fc30) 0
vptridx=0 vptr=((& B2::vtable for B2) + 24)
B (0x0x7ffad8f96b40) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& B2::vtable for B2) + 80)
Vtable for D
D::vtable for D: 20 entries
0 40
8 (int (*)(...))0
16 (int (*)(...))(& typeinfo for D)
24 (int (*)(...))D::f
32 (int (*)(...))D::f1
40 (int (*)(...))B1::Bf1
48 (int (*)(...))D::f2
56 (int (*)(...))D::Df
64 24
72 (int (*)(...))-16
80 (int (*)(...))(& typeinfo for D)
88 (int (*)(...))D::non-virtual thunk to D::f()
96 (int (*)(...))D::non-virtual thunk to D::f2()
104 (int (*)(...))B2::Bf2
112 0
120 18446744073709551576 // -40
128 (int (*)(...))-40
136 (int (*)(...))(& typeinfo for D)
144 (int (*)(...))D::virtual thunk to D::f()
152 (int (*)(...))B::Bf
VTT for D
D::VTT for D: 7 entries
0 ((& D::vtable for D) + 24)
8 ((& D::construction vtable for B1-in-D) + 24)
16 ((& D::construction vtable for B1-in-D) + 80)
24 ((& D::construction vtable for B2-in-D) + 24)
32 ((& D::construction vtable for B2-in-D) + 80)
40 ((& D::vtable for D) + 144)
48 ((& D::vtable for D) + 88)
Class D
size=56 align=8
base size=37 base align=8
D (0x0x7ffad8fc0150) 0
vptridx=0 vptr=((& D::vtable for D) + 24)
B1 (0x0x7ffad8f8fd00) 0
primary-for D (0x0x7ffad8fc0150)
subvttidx=8
B (0x0x7ffad8f96ea0) 40 virtual
vptridx=40 vbaseoffset=-24 vptr=((& D::vtable for D) + 144)
B2 (0x0x7ffad8f8fd68) 16
subvttidx=24 vptridx=48 vptr=((& D::vtable for D) + 88)
B (0x0x7ffad8f96ea0) alternative-path
</code></pre>
<h3 id="虚单继承">虚单继承</h3>
<p>暂时抛开其他部分,只关注<code>B->B1</code>这一条单继承&虚继承的结构。可以发现相比于普通的单继承,<code>B1</code>的虚表的每一段开头都新增了一项 <code>16</code>,而且继承类的虚表不再是重叠在基类的虚表上。那么虚继承为什么要给虚表新增一项?为什么本类的虚函数没有追加到基类的虚表尾部,而是独立一块?</p>
<p>首先我们知道虚继承的作用是使得在钻石型继承层次下(说普遍一点应该是共基类多继承),底层派生类中只存在一个虚基类子对象。如果子类直接使用<code>this</code>指针加上一个固定的偏移去访问虚基类中的成员的话,虚基类子对象的位置就会被固定下来,多个子类就一定会带上多个基类子对象。这与虚继承的功能是相违背的。</p>
<p>所以虚基类子对象的位置不能固定。通过<code>gdb</code>调试信息可知,虚基类子对象一般是放在子类对象的末尾。类<code>B</code>子对象到类<code>B1</code>子对象之间的距离确实不是固定的(类<code>D</code>还可以继承更多类)。执行一个指向<code>D</code>对象的<code>B1</code>指针调用的<code>B1</code>中没有被<code>D</code>重写的虚函数时(比如这里的<code>b1_ptr->Bf();</code>),需要动态地确定基类子对象的位置。</p>
<pre><code class="language-c++">
(gdb) x/7xg &d
0x7ffffffede20: 0x0000000008202b40 0x00000042000000b1 // vptr for B1 , {'B', 0xB1}
0x7ffffffede30: 0x0000000008202b80 0x00000043000000b2 // vptr for B2 , {'C', 0xB2}
0x7ffffffede40: 0x00000044000000dd 0x0000000008202bb8 // {'D', 0xDD} , vptr for B
0x7ffffffede50: 0x00007f41000000bb // {'A', 0xBB}
...
(gdb) x/4xg &b1
0x7ffffffede00: 0x0000000008202c68 0x00000042000000b1 // vptr for B1 , {'B', 0xB1}
0x7ffffffede10: 0x0000000008202ca0 0x00000041000000bb // vptr for B , {'A', 0xBB}
</code></pre>
<p>因此需要在虚表中新增一项,用于指出对象中的虚基类子对象相对于当前<code>this</code>指针的偏移值。每次访问虚基类的成员时都需要先读取该值,再将<code>this</code>加上此偏移才能得到虚基类子对象的地址。</p>
<p>以<a href="https://godbolt.org/z/Ntzexw"><code>B1::f1()</code></a>中的 <code>--cb;</code> 这条语句为例:</p>
<pre><code>...
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8] // this -> rax
mov rax, QWORD PTR [rax] // 虚表地址(首虚函数项地址) -> rax
sub rax, 24 // 向上寻 3 个虚表项
mov rax, QWORD PTR [rax] // 取得到虚基类子对象的偏移值
mov rdx, rax // rax -> rdx
mov rax, QWORD PTR [rbp-8] // this -> rax
add rax, rdx // 虚基类子对象地址 = this + 偏移值
movzx edx, BYTE PTR [rax+12] // 子对象地址 + 12 即为 B::cb 的地址
sub edx, 1 // --cb;
mov BYTE PTR [rax+12], dl // 寄存器 -> 内存
...
</code></pre>
<p>这就是虚基类带来的运行时性能损失。以及虚函数表的作用之二,定位虚基类子对象的地址。</p>
<p>同时这也解释了为什么虚继承会使得继承类的虚表中,继承类自己使用的虚函数不再和基类使用的虚函数重叠在一起,不像普通继承里那样追加表项到基类虚表尾部。这是因为继承类访问任何虚基类的成员变量,都需要经过类似上述汇编代码所述的将<code>this</code>加上一个偏移值的过程;而这个偏移值相对<code>B1*</code>和<code>B*</code>是不相同的,<code>B1*</code>需要加上<code>16</code>,而<code>B*</code>只需偏移<code>0</code>;所以两者不能共处,继承类和虚基类不再共用同一段虚表。在继承类的虚表的中间还有一项为<code>0</code>,我猜就是用来分隔两段的。</p>
<p>而这样的分离又导致了其他后果,且看下图:<br>
<img src="https://yang2096.github.io/post-images/1629730306201.bmp" alt="" loading="lazy"></p>
<p>一个指向<code>B1</code>对象的<code>B1*</code>类型的指针,调用继承而来的虚函数,所需要的步骤比它基类的指针调用这个函数多出这么多。来看看它多干了哪些活:</p>
<pre><code class="language-asm">mov rax, QWORD PTR [rbp-8] \
mov rax, QWORD PTR [rax] |
sub rax, 24 |
mov rax, QWORD PTR [rax] } 虚基类子对象地址 -> rdx
mov rdx, rax |
mov rax, QWORD PTR [rbp-8] |
add rdx, rax /
mov rax, QWORD PTR [rbp-8] \
mov rax, QWORD PTR [rax] |
sub rax, 24 |
mov rax, QWORD PTR [rax] |
mov rcx, rax |
mov rax, QWORD PTR [rbp-8] } 虚基类的第二个虚函数地址 -> rax
add rax, rcx |
mov rax, QWORD PTR [rax] |
add rax, 8 |
mov rax, QWORD PTR [rax] /
mov rdi, rdx
call rax
</code></pre>
<p>可以看出有虚继承时,没有重写的虚函数需要先将<code>B1 *this</code>转换成<code>B *this</code>才能调用。因为<code>B1</code>的虚表(前一段)里缺少它没有重写的虚函数,想要调用只能是<code>B*</code>来调用。可以说是虚函数调用中最繁琐的一类调用方式了。(这里居然重复计算了虚基类子对象的地址😳。不过这只是<code>g++</code>的一家之言,理解就好。)</p>
<h3 id="钻石虚继承">钻石虚继承</h3>
<p>在没有引入虚继承前,继承体系中的每个类在构造时只需要调用其直接基类的构造函数即可。基类自然会递归地调用它的基类的构造函数。</p>
<p>而引入虚继承后,虚基类的构造交给了继承体系中最底层的那个类。所以中间的那些类(或者说每一个含有虚基类的类)必须知道什么时候不去构造自己的虚基类。</p>
<p>具体到这个例子,<code>new</code>一个类<code>D</code>的对象时,<code>B</code>的构造函数应该由<code>D</code>的构造函数调用。中间的<code>B1</code>和<code>B2</code>不能重复构造类<code>B</code>。而<code>new</code>一个类<code>B1</code>时,它又需要自己去构造类<code>B</code>了。为了处理两种不同的构造需求——只构造自己和同时构造虚基类与自己——<code>g++</code>为含有虚基类的类生成了两个不同的构造函数,<code>base object constructor</code>和<code>complete object constructor</code>。前者专由该类的派生类调用。</p>
<p>虚继承的新增的两个结构 <code>VTT</code> 和 <code>Construction vtable</code> 正是为了处理虚基类的构造过程而引入的。</p>
<p><code>VTT</code>的意思是<code>virtual-table table</code>,用来索引虚表的表,其表项指向了各个虚表的不同位置。</p>
<p><code>Construction vtable for xx-in-yy</code> 顾名思义,就是专门给类<code>yy</code>在构造其基类<code>xx</code>时所使用的虚表。</p>
<p>先看看在类<code>D</code>的<code>complete object constructor</code>里这两个结构如何使用:</p>
<pre><code class="language-asm">mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
add rax, 40 // 虚基类子对象地址
mov rdi, rax
call B::B() [base object constructor] // 构造类 B
mov rax, QWORD PTR [rbp-8]
mov edx, OFFSET FLAT:VTT for D+8 // VTT for D 的第二项,是地址而不是内容(构造虚表)
mov rsi, rdx
mov rdi, rax
call B1::B1() [base object constructor] // 构造类 B1, 使用的是 base object constructor
mov rax, QWORD PTR [rbp-8]
add rax, 16
mov edx, OFFSET FLAT:VTT for D+24 // VTT for D 的第四项
mov rsi, rdx
mov rdi, rax
call B2::B2() [base object constructor] // 构造类 B2
mov edx, OFFSET FLAT:vtable for D+24
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], rdx // 类 D 中的 vptr for B1
mov rax, QWORD PTR [rbp-8]
add rax, 40
mov edx, OFFSET FLAT:vtable for D+144
mov QWORD PTR [rax], rdx
mov edx, OFFSET FLAT:vtable for D+88 // 类 D 中的 vptr for B
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax+16], rdx // 类 D 中的 vptr for B2
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax+32], 221 // 0xDD
mov rax, QWORD PTR [rbp-8]
mov BYTE PTR [rax+36], 68 // 'D'
</code></pre>
<p>可以看到类 <code>D</code> 在构造 <code>B1</code> 和 <code>B2</code> 的时候不仅使用的是 <code>base object constructor</code>,而且还将<code>construction vtable</code>的索引作为参数传入。那么这个构造虚表与普通虚表又有何不同呢?</p>
<p>为了便于比较,我把<code>Construction vtable for B1-in-D</code> 放到了 <code>Vtable for B1</code> 的右边,可以看到构造虚表中除了虚基类的偏移不同(主要作用)之外,其他表项全都相同,这意味着<code>B1</code>的构造函数中只能使用自己的虚函数。这也是《Effective C++》中条款9:“绝不在构造和析构函数中调用 virtual 函数”的原因,因为这个时候没有多态,虚函数的调用都是固定的。不可能也不可以在基类的构造函数中调用到派生类的虚函数——派生类的成员都还没有构造好。实际上条款 9 针对是所有继承方式而不只是虚继承,只是与构造虚表结合则更现突出。</p>
<p>至于<code>VTT</code>的作用,在这个继承体系中的作用其实还不太明显,完全可以直接传递构造虚表的地址给<code>base object constructor</code>。<code>VTT</code>服务于更加多层的继承体系,假如又有一个类<code>E</code>继承了类<code>D</code>,那么在类<code>D</code>的<code>base object constructor</code>中,需要类<code>E</code>提供的<code>Construction vtable for D-in-E</code> 以及 <code>Construction vtable for B1-in-E</code>,这个时候就不能只传入一个构造虚表的地址,而是传入<code>VTT</code>中的相应表项,就可以让<code>D</code>的<code>base object constructor</code>自己加上偏移找到<code>Construction vtable for B1-in-E</code>和<code>Construction vtable for B2-in-E</code>了。</p>
<h2 id="总结">总结</h2>
<p>通过分析编译期生成的各种结构以及各类虚函数的调用过程,可以较为清楚地了解到 C++ 底层对于多态性的支持。而 C++ 多态性的最主要支撑点就是虚函数表,它解决了类型识别,函数延迟绑定,共有基类子对象查找等问题。</p>
<p>本文仅分析了虚函数调用以及构造函数调用的情况,尚有<code>RTTI</code>及异常处理等多态性的语义未提及。本文的分析对象仅限于<code>gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)</code>所生成的数据,其他编译器生成的代码则多有不同,比如<code>MSVC</code>中就是直接将虚基类子对象的偏移插入到了对象当中。这些都有待继续探索。</p>
<h2 id="参考资料">参考资料</h2>
<p><a href="https://coolshell.cn/articles/12176.html">C++ 对象的内存布局 | | 酷 壳 - CoolShell</a><br>
<a href="https://shaharmike.com/cpp/vtable-part1/">C++ vtables - Part 1 - Basics | Shahar Mike's Web Spot</a><br>
<a href="https://www.cnblogs.com/BEN-LK/p/10720300.html">C++多态及其实现原理</a><br>
<a href="https://www.cnblogs.com/QG-whz/p/4909359.html">图说C++对象模型:对象内存布局详解</a><br>
<a href="https://blog.csdn.net/xiejingfa/article/details/48028491">从内存布局看C++虚继承的实现原理_C/C++_上善若水,人淡如菊-CSDN博客</a><br>
<a href="https://quuxplusone.github.io/blog/2019/09/30/what-is-the-vtt/">What is the virtual table table?</a></p>
]]></content>
</entry>
</feed>