第1章:对象模型初识

对齐原书第1章 Object Lessons:封装的布局成本、三种对象模型、继承后的表示、struct/class 差异、指针类型与多态语义。

学习目标

  • 能比较 C 数据抽象、simple object model、table-driven object model 与主流 C++ object model 的表示成本
  • 能解释 encapsulation 与 inheritance 加入后,哪些内容进入对象、哪些内容只存在于程序代码和 class metadata
  • 能推导 pointer 的 static type、object 的 dynamic type 对数据访问、非虚调用和 virtual dispatch 的不同结果

从“类会不会让对象变大”开始

原书先拆掉一个常见误解:把 C 的 struct + free functions 改写为 C++ class + member functions,并不意味着每个 object 都保存一份 function code。先预测两种写法的 sizeof 是否相同,再观察 compiler 输出;真正影响 object size 的通常是 nonstatic data、alignment、base subobject,以及 implementation 为 virtual function 或 virtual inheritance 引入的元数据。

下面两种写法都需要保存三个 float。第二种写法增加语言层关系和访问控制,却不要求把 translate 的机器码复制进每个实例。

struct CPoint3d {
    float x;
    float y;
    float z;
};
 
void translate(CPoint3d* point, float delta) {
    point->x += delta;
}
 
class Point3d {
public:
    void translate(float delta) { x_ += delta; }
 
private:
    float x_;
    float y_;
    float z_;
};

编译器可以把 nonstatic member call 概念性地变换为接收隐藏 this 参数的普通函数调用。encapsulation 的主要成本因此不是“成员函数占对象空间”,而是访问规则、可能的 accessor call,以及 ABI 不能随意改变 private layout 的工程约束。inline 后 accessor 甚至可能不留下 call。

四种表示方式为何逐步收敛

simple object model 很规整:所有 member 都通过 slot 间接访问。但一个有很多 member functions 的 object 会因 function slots 变大,每次 data access 也多一次 indirection。它适合解释“class 是 member 集合”,不适合作为高效实现的常见选择。

table-driven model 把 member slots 移到共享表里,object 变小,却让普通数据访问也必须查表。主流 C++ model 选择更偏向 common case 的折中:

  1. nonstatic data members 直接放在 object 内,以编译期 offset 访问;
  2. static data members 在对象之外只有一份;
  3. static 与 nonstatic member function code 都在对象之外;
  4. polymorphic class 常由 implementation 在对象中放 vptr,共享 class-specific vtable;
  5. 没使用 virtual mechanism 的类,不应仅因“是 class”就支付 virtual dispatch 成本。

标准规定 observable behavior,不规定 vptr 必须位于第一个字节,也不规定 vtable 的精确 shape。书中讲的是当时及后来 ABI 广泛采用的 implementation model。分析内存转储时必须标注 compiler、target、ABI 与 options,不能把某次实验结果误写成语言保证。

加入继承后,对象里出现什么

普通 single inheritance 通常把 base subobject 安置在 derived object 的固定位置,再放 derived data。转换 Derived* 到 unambiguous non-virtual Base* 时,implementation 可使用固定 offset;如果 base 位于起始位置,offset 甚至为零。

struct Point2d {
    float x;
    float y;
};
 
struct Point3d : Point2d {
    float z;
};

这段代码的语义要求 Point3d 含有一个可作为 Point2d 使用的 base subobject。它不保证无 padding,也不保证所有 compiler 采用相同 bytes。多重继承会有多个 base views;virtual inheritance 则要求多条路径共享一个 virtual base subobject,访问 offset 可能依赖动态布局信息。第 3、4、5 章会分别展开 data、function 和 construction 代价。

对象模型怎样影响程序

object model 直接影响 sizeof、pointer adjustment、member access、virtual call、construction order 与 binary interface。它也影响优化器能否把抽象还原为直接操作:若 concrete type 和 call target 可证明,compiler 可能 devirtualize;若 dynamic target 不可知,就必须保留 runtime choice。

void moveX(Point3d& point, float delta) {
    point.translate(delta);
}

translate 非虚且 definition 可见,调用可能 inline 为一次 load/add/store。若接口改成 virtual 且实际 dynamic type 不可证明,常见 lower form 是 load vptr、load slot、indirect call。高层写法相似,机器路径由语义契约而不是关键字数量决定。

class 与 struct 的关键字差异

class 默认 private member/private inheritance,struct 默认 public member/public inheritance。二者都能有 constructor、virtual function、base class、template 和 access specifier。所谓“struct 是 C layout、class 才有 C++ layout”并不成立。

struct PublicByDefault {
    int value;
};
 
class PrivateByDefault {
    int value;
};

工程上可约定 struct 表示 passive value、class 表示 invariant-bearing abstraction,但这是 style,不是另一套 runtime model。C interoperability 还需要满足 standard-layout、triviality、calling convention 等额外约束,不能仅靠改关键字保证。

object distinction:对象与观察它的指针不是一回事

Base*Derived* 都是地址型 handle,但 type 决定 compiler 如何解释该地址、允许哪些 member、需要什么 pointer adjustment。把 derived address 转为 base pointer,不会把 object 改造成 Base;它建立了一个只观察 base subobject 的静态视图。

struct Shape {
    int color = 0;
    void inspect() const {}
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
 
struct Circle final : Shape {
    double radius = 1.0;
    double area() const override { return 3.14159 * radius * radius; }
};
 
Circle circle;
Shape* view = &circle;
view->inspect(); // nonvirtual: Shape::inspect
view->area();    // virtual: Circle::area

data member access 也不因 dynamic type 自动切换:view->color 指向 Shape subobject 的字段。polymorphism 主要通过 pointer/reference 加上 virtual interface 生效;按值把 Circle 复制到 Shape object 会 slicing,丢失 derived state,而且抽象基类通常直接禁止这种复制。

第1章验证清单

  1. 对每个 class 标出 nonstatic data、static data、nonstatic functions 与 virtual metadata 的归属。
  2. 分清 language guarantee、ABI convention 与某次 compiler experiment。
  3. 预测 encapsulation 前后 object size 是否真正改变,并说明原因。
  4. 把 derived object 拆成 base subobjects、derived state 与 virtual-inheritance support。
  5. 对每个 expression 写出 static type 和可能的 dynamic type。
  6. 分别推导 data access、nonvirtual call 与 virtual call 的决议路径。
  7. 检查按值传递/赋值是否造成 object slicing。
  8. sizeof、address delta、optimized assembly 和 runtime output 验证,而不是把猜测当标准。

小结

  • encapsulation 不会把 member function code 复制进每个对象,真实成本来自 representation 与约束
  • simple model 和 table-driven model 揭示了统一性与 indirection 的取舍
  • 主流 C++ object model 以内联 nonstatic data 服务 common case,以 virtual table 支持 runtime choice
  • inheritance 在 most-derived object 内加入 base subobject,多重和虚继承可能需要 pointer adjustment
  • structclass 共享同一对象模型,默认访问权限才是语言层主要差别
  • static type 决定可见接口、数据和非虚决议,dynamic type 只在特定 runtime mechanism 中参与
  • virtual semantics 不等于每个 call site 都必须保留间接调用,optimizer 可以在有证据时去虚化
  • object slicing 是按值丢失 derived state,不是“多态复制”

资料与写作方式声明

本章以Inside the C++ Object Model, First Edition, Chapter 1: Object Lessons权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

data abstraction

把数据表示和操作组织成受保护接口。

layout costs for adding encapsulation

封装引入的布局、访问与 ABI 约束成本。

simple object model

对象用逐成员槽指向数据和函数实体的概念模型。

table-driven object model

对象经数据表和函数表间接查找成员的概念模型。

C++ object model

数据内联、动态能力按需付费的常见对象表示。

base subobject

派生对象中代表一个基类实例的存储部分。

keyword distinction

class 与 struct 的默认访问规则差异。

static type
表达式在编译期确定的观察类型。
dynamic type

运行期 most-derived object 的实际类型。

object slicing

按基类值复制时截去派生部分。

练习

  1. 问题 1:比较四种对象模型。 对一个含 3 个数据成员、8 个普通函数和 2 个虚函数的类,说明每种模型把什么放进单个 object、什么可共享,以及一次数据访问要经过几层间接。
  1. 问题 2:推导基类视图。Base* p = &derived,分别推导 p->datap->nonVirtual()p->virtualFn(),并说明何处使用 static type、何处使用 dynamic type。
  1. 问题 3:设计一次可重复布局实验。sizeof、成员地址差、base pointer conversion 和 virtual call 观察一个普通类、单继承类、多态类;列出哪些结论只能属于当前 ABI。

讨论

评论区加载中…