Inside C++ Object Model 读书笔记: Part2
(P)review: C++ 继承和设计
virtual
虚函数机制和继承几乎总是一起提的。编译器和链接器会保证:
- 对象和(包括虚函数的)函数的正确关联。
这一点提供了运行期的多态。顺便提一嘴,实际上在 C++ 上运行期多态和编译器多态还是很清晰的,如果你没写过 C++ 直接去写 Rust 的话,可能会对下面的区别感到头晕:
1 | trait Sample { |
我们可以回顾一下上一个笔记写的:
你需要/可能 override
掉 virtual
, 建议显式这么做。(overload
= override
)。同时也记住 final
这个关键字。
返回类型放松
C++ 返回是协变的,这意味着:
1 | class Expr { |
可以直接写:
1 | class Cond: public Expr { |
实际上这个还是很常见的。
而一定的逆变性来自于:基类的指针可以承载子类对象。
virtual base class
这是 C++ Reference 中的建议。建议认为,虚基类应该为了“共享对象”而承载成员。
虚基类的构造语义有下列的补充:
- 虚基类构造函数保证只调用一次,由最终对象调用
- 虚基类的构造函数会在派生类的构造函数之前呗调用。
在 C++ 中,基类可以为了下列的目的或者混合的目的:
- 接口继承
- 实现继承
书上比较有意思的是写了个 lval_box 的例子,这里用 protect 来继承 implement,用 public 继承接口。
书上给了个很有趣的例子:
1 | class X: public virtual Interface, protected Implement { |
在继承链中把 interface 当成 virtual class 使用。
同时,关于虚继承有下列一些特点:
所有虚基类子对象都在任何非虚基类子对象之前初始化,故只有最终派生类会在其成员初始化器列表中调用虚基类的构造函数
Data 语义学
NonStatic Data Members
当对一个 nonstatic data member 做 load/store 的时候,编译器的基本操作会是:
- 编译时已经知道对应的地址的偏移量
- 效率等同于存取 C struct member
在引入虚拟继承之前,代价都是一致的,在引入虚拟继承后,如果通过 virtual base class 的引用访问它的可访问成员,那么这会在运行时决定具体访问的对象。(问题:在继承/多继承的层次中,基类指针是怎么表示的?怎么样导致这种 bias 的?)
当然,编译器可能有一定的上下文,来优化掉这种开销。
继承与 data member
实现的时候,如果没有 virtual base class,一个子类的对象布局类似于:
1 | base class members |
一般来说,非 virtual base member 的基类成员,都会在对象的头部出现。
当 user 不需要运行时多态,即不使用 virtual class 的时候,对象会被”拼接“在一起。不过下面有几个疑问:
- 如何处理 padding ?
- 如何支持 RTTI?
1 | // The parts below means 3.1 in the projects. |
这回可不是 ebo 了!
- 组合的时候,clang 实现上还是会保证 padding 的
- 继承的时候,可能会优化掉可能的 padding
书上的代码成书的时候实现还是比较类似组合这样的。接下来再对我们上述贴的类型进行一点小实验:
1 |
|
如果你没看前面的内容,只是自己写这些代码的话,上面的内容应该是非常符合直觉的,*p21 = *p22
如果改了 d_temp.f3
的话怎么想都是不合适的。但是你知道 DerivedConcrete2
和 DerivedConcrete3
一样大之后,感觉就会奇怪起来。
实际上,可以在 cppreference 上找到子类型相关的定义:
- https://en.cppreference.com/w/cpp/language/object#Subobjects
- https://en.cppreference.com/w/cpp/types/is_trivially_copyable
In general, for any trivially copyable type
T
and an objectobj1
ofT
, the underlying bytes ofobj1
can be copied (e.g. by means of std::memcpy or std::memmove) into an array ofchar
,unsigned char
orstd::byte
or intoobj2
, a distinct object ofT
. Neitherobj1
norobj2
may be a potentially-overlapping subobject.
再加上 non-virtual 的多态后,对象需要添加一个 word 大小的 vptr,并且为这个类生成一个 vtable. 现在很多编译器将其置放在对象的起始部位。在多继承中,上述两种机制被混合起来,对象一个个排列,并且遵照之前说的方式进行。
但是这种转化有一个疑问就是,转化之后指针赋值应该怎么处理呢:
1 | class Base1 { |
实际上,这里做了一个地址的 cast。
引入虚拟继承
在引入虚拟继承之后,class 会生成一个 vbase
,同时动态的决定 vbase 对象的位置。同时,继承自 vbase 的类,会带有一个 vbase 对象的 ptrdiff。这种情况下,多个对象分部的 bias 会被指向同一个具体的布局位置。这种情况下,对象带来了一次 bias 寻址的开销。