一、RP 是什么?为什么要重新做一遍?

RP == Runtime Polymorphism(运行时多态)
传统 C++ 的 RP 手段只有两样:

virtual 函数
+ 继承(基类指针 / 引用)

也就是:

struct Shape {
    virtual ~Shape() = default;
    virtual double area() const = 0;
};
struct Circle : Shape {
    double r;
    double area() const override { return 3.14159 * r * r; }
};

传统 RP 的问题

  1. 必须用继承
  2. 必须有虚表(vptr)
  3. 必须通过指针 / 引用使用
  4. 对象语义丢失
  5. 很难组合(composition)
  6. 性能不可控(间接调用)
  7. ABI / ODR / 代码膨胀问题
    Rust 社区对此的回应是:Traits

二、这段话在说什么(逐条 + 解读)

1⃣

Instead of the feature for RP in the language, virtual and inheritance,
理解:
我们不使用语言内建的运行时多态机制

  • 不用 virtual
  • 不用继承体系

注意:
不是说它们“坏”,而是它们不是唯一的解法

2⃣

we will use user-code (a type erasure framework),
理解:
我们改用用户代码实现的多态框架,也就是:
Type Erasure(类型擦除)
核心思想:
把“接口”从“继承关系”中拆出来,用组合 + 间接调用表达

3⃣

that allows us to synthesize benefits from the Rust feature to do RP, Rust Traits,
理解:
这种做法可以在 C++ 中合成(synthesize)Rust Traits 的优点


Rust Trait 特性 C++ Type Erasure
无继承层级 无继承
行为约束 行为约束
可静态 / 动态 可静态 / 动态
对象安全 对象安全
可组合 可组合

4⃣

still in C++.
理解:
重点强调
不是“像 Rust”,不是“用 Rust 写”,而是:
100% C++,零语言扩展

5⃣

If we survive the challenge of understanding all of necessary things really well,
理解:
这不是免费的午餐:

  • 模板
  • 值语义
  • ABI
  • 对象生命周期
  • inline / devirtualization
  • small buffer optimization
    理解成本非常高

6⃣

we get “best of all worlds”:
意思是:
如果你真的掌握了,就能同时拥有多种语言的优势。

三、“Best of all worlds” 是什么?

✓ 1. Better performance, object code size

为什么性能更好?
  • 可以:
    • inline
    • 去虚表
    • 控制内存布局
  • 小对象可栈分配
  • 可用 SBO(small buffer optimization)
    关键点:

多态 ≠ 一定要虚函数

✓ 2. More modeling power

建模能力指什么?

你不再被迫:

“这个类型必须继承自 X”

而是:

“这个类型只要满足行为约束即可”

这和 Rust Trait / Haskell typeclass 是同一思想。

四、这怎么可能?

原文:

How’s this possible?

1⃣

Because of C++: Performance preserving/increasing abstraction
理解:
C++ 的核心设计目标之一:
抽象不应该降低性能
这正是 type erasure 能成功的原因。

2⃣

Because of the wisdom of not committing to “the language designers know best”
理解(很 C++ 社区)
C++ 不强迫你“只能用语言给的那一套”
你可以:

  • 绕开语言特性
  • 用库表达语义
  • 自己搭抽象

3⃣

Because of our resistance for outstanding complexity
理解:
虽然方案复杂,但:

  • 复杂性是显式的
  • 可被隔离在库中
  • 使用者接口可以非常干净

4⃣

I hope this shows you the value of these C++ conferences
这不是炫技,是经验共享

5⃣

We gather to help each other do what seems impossible!
典型 C++ 大会精神
——「语言没给?我们自己造。」

五、最小可运行示例(Type Erasure,多态但无继承)

目标

我们要表达:

“任何有 draw() 行为的类型,都可以被当作 Drawable 使用”

1⃣ 行为接口(非继承)

// 纯“概念”,不暴露给用户
struct DrawableConcept {
    void (*draw)(const void*);
};

2⃣ 模型包装(模板)

template<typename T>
struct DrawableModel {
    static void draw_impl(const void* self) {
        // 将 void* 转回真实类型
        static_cast<const T*>(self)->draw();
    }
    static constexpr DrawableConcept table {
        &draw_impl
    };
};

3⃣ 类型擦除对象(值语义)

class Drawable {
public:
    template<typename T>
    Drawable(T obj)
        : object_(new T(std::move(obj)))
        , concept_(&DrawableModel<T>::table)
    {}
    void draw() const {
        concept_->draw(object_);
    }
    ~Drawable() {
        delete_object();
    }
private:
    void* object_;
    const DrawableConcept* concept_;
    void delete_object() {
        // 示例简化:真实实现会存 deleter
        delete static_cast<char*>(object_);
    }
};

4⃣ 用户类型(完全不知道多态存在)

struct Circle {
    void draw() const {
        std::cout << "Draw Circle\n";
    }
};
struct Square {
    void draw() const {
        std::cout << "Draw Square\n";
    }
};

5⃣ 使用方式(无继承、无 virtual)

int main() {
    Drawable d1 = Circle{};
    Drawable d2 = Square{};
    d1.draw();
    d2.draw();
}

六、总结一句话

这段话真正想说的是:
在 C++ 里,
运行时多态 ≠ virtual + inheritance

通过 type erasure + 值语义 + 库抽象
我们可以在 C++ 中实现 Rust Traits 级别的建模能力
同时保留 更好的性能与控制力

一、整体设计在做什么(先给全景)

你这份代码实现的是:

不用 virtual / inheritance
仅用普通 C++ 代码,
构造一个“像 Rust trait object 一样”的 Runtime Polymorphism(RP)系统

核心思想一句话总结:
把“我需要的行为”抽象成一张函数指针表(vtable),
每个具体类型 T 提供自己的实现,
对象本身用 void* 擦除类型。

这正是:

  • Rust dyn Trait
  • Swift protocol witness table
  • C ABI 插件系统
  • std::function / std::any 的底层思想

二、逐段详细理解 + 注释

1⃣ DrawableConcept<T> —— 行为“接口描述”

template <typename T>
// Concept = "我需要哪些行为"
// 注意:这是纯数据结构,不是多态基类
struct DrawableConcept {
    // 指向 draw 行为的函数指针
    void (*draw)(const void*);
};

理解

这里的 Concept 不是 C++20 concept,而是概念模型上的 concept

“任何 Drawable 的类型,必须能做什么?”
你只要求了一件事:

  • draw()
    因此这个结构里只放了一个函数指针:
void (*draw)(const void*);

注意几个关键点:

  • ✗ 没有虚函数
  • ✗ 没有继承
  • ✗ 没有对象状态
  • ✓ 只是“行为签名集合”

这就是 runtime interface,不是类型层次。

2⃣ DrawableModule<T> —— 每个类型的“适配器 + vtable”

template <typename T>
struct DrawableModule {
    // 适配函数:把 void* 转回 T,然后调用 T::draw()
    static void draw_impl(const void* self) {
        static_cast<const T*>(self)->draw();
    }
    // 每个 T 对应一张"行为表"
    static constexpr DrawableConcept<T> vtable = {&draw_impl};
};

理解

这是整个系统的桥梁层(adapter layer)
它做了两件事:

✓ 1. 把 “void* 世界” 转回 “类型世界”

static_cast<const T*>(self)->draw();
  • Drawable 里只知道 void*
  • 这里恢复成 T*
  • 调用真正的 T::draw()

✓ 2. 为每个 T 生成一张静态 vtable

static constexpr DrawableConcept<T> vtable

非常重要的一点:

✓ 每个 T 只有一张 vtable
✓ vtable 在编译期生成
✓ 所有同类型对象共享这张表
这正是传统 virtual vtable 的等价物。

3⃣ Drawable —— type-erased 对象本体

class Drawable {
public:

这是你真正拿在手里用的“多态对象”。

3.1 构造函数:类型擦除发生的地方

template <typename T>
Drawable(T obj)
    : object_(new T(std::move(obj))),
      vtable_(reinterpret_cast<const void*>(&DrawableModule<T>::vtable)),
      deleter_(&delete_impl<T>),
      cloner_(&clone_impl<T>) {}
理解

这一段完成了 4 件关键事情

  1. 把具体对象堆分配
    object_ = new T(...)
    
    → 保存为 void*
  2. 记录“行为表”
    &DrawableModule<T>::vtable
    
    → 这是 T 的“能力说明书”
  3. 记录正确的删除方式
    delete_impl<T>
    
  4. 记录正确的拷贝方式
    clone_impl<T>
    

这一刻起:
Drawable 不再关心 T 是什么,只关心“怎么用它”

3.2 拷贝构造:深拷贝

Drawable(const Drawable& other)
    : object_(other.cloner_(other.object_)),
      vtable_(other.vtable_),
      deleter_(other.deleter_),
      cloner_(other.cloner_) {}
理解

这是 比传统虚函数更强的地方

  • 虚基类通常 无法深拷贝
  • 你这里是 值语义
    逻辑是:

“用对方提供的 clone 方法,
拷贝出一个同类型的新对象。”

3.3 移动构造 & 赋值 —— 所有权转移

你这里实现得 完全正确,关键点是:

other.object_ = nullptr;

否则就会 double-free。
这是手写 type erasure 必须非常小心的地方

3.4 draw() —— 统一的多态调用点

void draw() const {
    auto* concept_ptr =
        reinterpret_cast<const DrawableConcept<void>*>(vtable_);
    concept_ptr->draw(object_);
}
理解

调用链是:

Drawable::draw()
  → vtable->draw(void*)
      → DrawableModule<T>::draw_impl
          → T::draw()

这和:

base->virtual_func();

机器码层面是等价的

4⃣ 用户类型:完全无侵入

struct Circle {
    double r;
    void draw() const { ... }
};

理解

这是这个设计的巨大优势

  • 不需要继承
  • 不需要虚析构
  • 不需要改已有代码
  • 第三方类型也能用

5⃣ main() —— 使用层面验证设计

你这里已经把 所有关键能力都测试到了

  • 基本调用
  • 拷贝
  • 容器
  • 赋值
  • 多种类型
    这说明你的 abstraction 是完整的

三、你这版的两个“高级评价”

✓ 优点 1:你已经在做 Rust trait object

你这套模型,本质等价于:

let x: Box<dyn Drawable>

而且:

  • 更灵活(可自定义 clone / destroy)
  • ABI 可控
  • 无继承耦合

一个隐藏的风险(重要)

reinterpret_cast<const DrawableConcept<void>*>

auto* concept_ptr =
    reinterpret_cast<const DrawableConcept<void>*>(vtable_);

这里在语言层面是 UB(严格别名规则),因为:

  • DrawableConcept<T>
  • DrawableConcept<void>
    在 C++ 看来是 不同类型

工业级做法有两种:

✓ 做法 A(推荐):Concept 非模板
struct DrawableConcept {
    void (*draw)(const void*);
};
✓ 做法 B:用 std::byte / void* 存储并只访问函数指针

你现在的写法 在主流编译器上是可工作的
但如果你追求“标准洁癖级正确”,需要改这一点。

四、总结一句话

你已经完整实现了一个
“无 virtual、无继承、值语义、可放容器的 Runtime Polymorphism 框架”

这已经是 CppCon / Herb Sutter / Rust 交叉区的内容,不是玩具。

#include <iostream>
#include <utility>
#include <vector>
template <typename T>
// Concept = "我需要哪些行为"
// 注意:这是纯数据结构,不是多态基类
struct DrawableConcept {
    // 指向 draw 行为的函数指针
    void (*draw)(const void*);
};
template <typename T>
struct DrawableModule {
    // 适配函数:把 void* 转回 T,然后调用 T::draw()
    static void draw_impl(const void* self) {  //
        static_cast<const T*>(self)->draw();
    }
    // 每个 T 对应一张"行为表"
    static constexpr DrawableConcept<T> vtable = {&draw_impl};
};
class Drawable {
public:
    // 构造函数:接受任意满足 draw() 的类型
    template <typename T>
    Drawable(T obj)
        : object_(new T(std::move(obj))),
          vtable_(reinterpret_cast<const void*>(&DrawableModule<T>::vtable)),
          deleter_(&delete_impl<T>),
          cloner_(&clone_impl<T>) {}
    // 可拷贝(深拷贝)
    Drawable(const Drawable& other)
        : object_(other.cloner_(other.object_)),
          vtable_(other.vtable_),
          deleter_(other.deleter_),
          cloner_(other.cloner_) {}
    // 可移动 - 手动实现以正确转移所有权
    Drawable(Drawable&& other) noexcept
        : object_(other.object_),
          vtable_(other.vtable_),
          deleter_(other.deleter_),
          cloner_(other.cloner_) {
        other.object_ = nullptr;  // 关键:转移所有权后清空源对象
    }
    // 赋值运算符
    Drawable& operator=(const Drawable& other) {
        if (this != &other) {
            if (object_) {
                deleter_(object_);
            }
            object_ = other.cloner_(other.object_);
            vtable_ = other.vtable_;
            deleter_ = other.deleter_;
            cloner_ = other.cloner_;
        }
        return *this;
    }
    Drawable& operator=(Drawable&& other) noexcept {
        if (this != &other) {
            if (object_) {
                deleter_(object_);
            }
            object_ = other.object_;
            vtable_ = other.vtable_;
            deleter_ = other.deleter_;
            cloner_ = other.cloner_;
            other.object_ = nullptr;  // 关键:转移所有权后清空源对象
        }
        return *this;
    }
    // 析构
    ~Drawable() {
        if (object_) {
            deleter_(object_);
        }
    }
    // 统一的多态调用接口
    void draw() const {
        // 通过函数指针表调用具体类型的 draw
        auto* concept_ptr = reinterpret_cast<const DrawableConcept<void>*>(vtable_);
        concept_ptr->draw(object_);
    }
private:
    void* object_;                  // 被擦除的对象
    const void* vtable_;            // 行为表 (type-erased)
    void (*deleter_)(void*);        // 删除器
    void* (*cloner_)(const void*);  // 拷贝器
    template <typename T>
    static void delete_impl(void* p) {
        delete static_cast<T*>(p);
    }
    template <typename T>
    static void* clone_impl(const void* p) {
        return new T(*static_cast<const T*>(p));
    }
};
// 测试类型
struct Circle {
    double r;
    void draw() const { std::cout << "Draw Circle, r = " << r << "\n"; }
};
struct Square {
    double a;
    void draw() const { std::cout << "Draw Square, a = " << a << "\n"; }
};
struct Triangle {
    double base, height;
    void draw() const {
        std::cout << "Draw Triangle, base = " << base << ", height = " << height << "\n";
    }
};
int main() {
    std::cout << "=== Basic Usage ===\n";
    Drawable d1 = Circle{3.0};
    Drawable d2 = Square{5.0};
    d1.draw();
    d2.draw();
    std::cout << "\n=== Copy Semantics ===\n";
    Drawable d3 = d1;  // 深拷贝
    d3.draw();
    std::cout << "\n=== Container Usage ===\n";
    // 可以放进容器 - 这是传统虚函数无法做到的灵活性
    std::vector<Drawable> shapes;
    shapes.emplace_back(Circle{1.0});
    shapes.emplace_back(Square{2.0});
    shapes.emplace_back(Triangle{3.0, 4.0});
    for (const auto& s : shapes) {
        s.draw();
    }
    std::cout << "\n=== Assignment ===\n";
    Drawable d4 = Circle{10.0};
    d4 = Square{20.0};  // 赋值
    d4.draw();
    return 0;
}

https://godbolt.org/z/bxrfbxW1o

一、像“语言设计者”一样思考

these things as if we were language designers
这句话非常关键,意思是:
不是“C++ 现在有什么特性我能用”
而是“如果我要设计一门语言,我会保留哪些本质?”
这是一种抽象层次的切换

  • 从“程序员使用者”
  • 切换到“语言机制设计者”

二、明确:我们不做什么(Out of scope)

这是非常“成熟的设计态度”

✗ 明确排除的内容

1⃣ Rust Traits 的编译期能力
  • 单态化(monomorphization)
  • trait bounds
  • compile-time specialization
    只讨论 Runtime Polymorphism(RP)
2⃣ Rust enum 风格的 RP
  • 类似 std::variant
  • 值分发,不是行为分发
    这是 另一种多态模型
3⃣ 不追求“覆盖所有特性”
  • 不是“Rust Trait in C++ 完整复制”
  • 而是 抓住最核心的可复用抽象
4⃣ 不讨论 Rust ↔ C++ 绑定
  • 那是 FFI 领域
  • 不是抽象模型本身
5⃣ 目标不是“教你怎么 Rust → C++”

而是:

“Rust 的某些好点子,本质是什么?”

三、核心问题:Rust Traits 的 RP「风格」是什么?

What Is the “style” of RP of Rust Traits?
这里的 style 不是语法,是抽象方式
Rust trait 的 RP 并不是:

  • 继承
  • 虚基类
  • 子类关系
    而是:

“我有一组能力,你只要满足它,我就能用你”
这在 Rust 里叫:

dyn Trait

四、什么是 RP?为什么它有价值?

RP == Runtime Polymorphism

你已经在代码里做到了,我们用概念再精炼一次。

Runtime(运行期)

The actual implementation is not known when compiling
意思是:

  • 编译期 不知道具体类型
  • 运行期 才决定用哪个实现
    这点和模板、concept 正好相反。

Polymorphism(多态)

That we can substitute one implementation for another
即:

  • 替换实现
  • 不破坏程序正确性
    这正是下面要讲的 LSP

五、Liskov Substitution Principle(里氏替换原则)

这是“多态”在理论上的定义

原意(简化版)

如果 ST 的一种实现:

那么所有使用 T 的地方
都可以安全地使用 S
换成你这次的上下文:
任何实现了 trait / interface 的类型
都能被当成该 interface 使用

非常重要的一点

LSP 并不要求继承
它只要求:

  • 行为一致
  • 语义一致
    这为 “不使用继承的多态” 打开了大门。

六、问题其实很简单(但常被搞复杂)

The Issue Is Quite Simple
这是典型的 Herb Sutter / Alexandrescu 风格。

1⃣ 识别“本质”,丢掉无关细节

Identify the essentials
对于 RP,本质只有:

  • 一组行为(函数)
  • 一种统一调用方式
  • 若干具体实现
    其他都是实现细节。

2⃣ 把本质收敛成一个“公共物”

Gather the essentials into something common
这正是你代码里的:

DrawableConcept

七、为什么抽象如此重要(N × M 问题)

没有抽象:

N implementations × M uses
= N * M 份耦合代码

每多一个实现、一个使用点,复杂度爆炸。

有抽象(Interface / Trait):

1 interface
+ N implementations
+ M uses

这是数量级的胜利

八、如果我们做对了,会发生什么?

If we get it right, huge win!

赢在三点:

1⃣ 复杂度下降(结构性胜利)
2⃣ 概念复用(conceptual leverage)
3⃣ 实现自由(不绑死语言特性)

九、关键转折点:子类型 ≠ 子类

这是整场演讲最重要的一句话之一

在大多数语言(包括 C++)里:

  • Subtyping(子类型)
  • 错误地等同为
  • Subclassing(继承)

作者要做的事是:

把“子类型”从“继承”里解放出来
也就是:

Type: Interface
Subtype: Implementation

而不是:

Base class
↓
Derived class

十、明确:我们不会做什么

Subclassing (base class and virtual overrides): what we won’t be doing
这句话就是在给你刚才那份代码背书

  • 不用 virtual
  • 不用 inheritance
  • 不用语言内建 RP
    而是:

用库 + 设计,实现 trait 风格 RP

十一、一句话总总结

这整段内容,其实在回答一个问题:
“如果我们不被 C++ 的历史包袱限制,
只保留多态的数学与工程本质,
RP 应该长什么样?”

而你刚才写的那套 type-erasure Drawable
正是这个答案的第一个具体实例

31⃣ 先定义一个“序列化接口”

struct ISerialize {
    virtual std::ostream&
    serialize(std::ostream&) const = 0;
    virtual std::size_t
    length() const = 0;
    virtual ~ISerialize() {}
};

理解

这是最传统、最标准的 C++ Runtime Polymorphism

  • 一个抽象基类
  • 两个纯虚函数
  • 一个虚析构函数

逐条解释

virtual std::ostream& serialize(std::ostream&) const = 0;
  • 行为 1:把对象写入输出流
  • 返回 ostream& 以支持链式调用
  • const:不修改对象
virtual std::size_t length() const = 0;
  • 行为 2:序列化后字符串的长度
  • 这是额外接口,用于提前分配 buffer 等
virtual ~ISerialize() {}
  • 必须是虚析构
  • 否则通过基类指针删除会 UB

32⃣ 我们想序列化一个整数

…an integer
直觉上你会想:

int x = 42;
serialize(x);

但……

33⃣ 问题来了:int 不能继承接口

But int does not inherit from ISerialize…

这是结构性问题

  • int内建类型
  • 无法修改它
  • 更不可能让它继承你的接口
    这是 OOP + 继承模型的第一堵墙

34⃣ 解决方案:包一层(Wrapper)

Then we wrap our integers into a wrapper type
于是我们发明一个“代理对象”:

struct SerializeWrapperInt : ISerialize {
    int value;
};
  • 真正的数据在里面
  • 外壳用来“继承接口”

35⃣ 那干脆写成模板吧

We’re cool, we will write the integer wrapper as a template
既然要包很多类型:

  • int
  • double
  • std::string
  • 用户自定义类型
    那就:
template<typename T>
struct SerializeWrapper;

36⃣ 模板序列化包装器:完整代码

template<typename T>
struct SerializeWrapper : ISerialize {
    T value_;
    virtual std::ostream&
    serialize(std::ostream& to) const override {
        return to << g_registry.id<T>() << ':' << value_;
    }
    virtual std::size_t
    length() const override {
        std::ostringstream temporary;
        serialize(temporary);
        return temporary.str().length();
    }
};

逐行注释

template<typename T>
struct SerializeWrapper : ISerialize {
  • SerializeWrapper<T> 是 ISerialize 的子类
  • 这就是“用继承实现能力”
T value_;
  • 被包装的真实对象
  • 真正的数据
return to << g_registry.id<T>() << ':' << value_;
  • 输出格式类似:
<type-id>:<value>
  • g_registry.id<T>():类型 → 唯一 ID
  • 用于反序列化
std::ostringstream temporary;
serialize(temporary);
return temporary.str().length();
  • 通过“真序列化”来计算长度
  • 正确,但:
    • 效率低
    • 有副作用
    • 不可缓存

37⃣ 问题总结:序列化 = 继承,真的好吗?

这一页是整段的思想高潮

✗ 问题 1:已有类型不能继承

Pre-existing types cannot inherit from this interface

  • int
  • std::string
  • 第三方库类型
    只能 wrap
    到处都是 wrapper
    类型膨胀

✗ 问题 2:你把类型“绑死”在一个继承体系里

If you make your type in the “ISerialize” hierarchy…
一旦你这样写:

struct MyType : ISerialize { ... }

你就做了一个不可逆的决定

✗ 后果 A:别人想要另一种序列化方式

例如:

  • JSON
  • Protobuf
  • FlatBuffers
    问题是:

一个类型只能继承一个基类
你不能:

struct MyType : ISerialize, IJsonSerialize, IProtobufSerialize; // 

✗ 后果 B:你想参与别的继承体系

比如:

struct MyType : QWidget { ... }

但你已经:

struct MyType : ISerialize { ... }

继承冲突
设计被锁死

为什么 Rust Traits 没这些问题?

I like that Rust Traits don’t have these problems!

因为 Rust Traits:

  • 不是继承
  • 不是对象层级
  • ✓ 是能力的声明
  • ✓ 是实现与类型解耦
    一个类型可以:
impl Serialize for i32
impl JsonSerialize for i32
impl Debug for i32

互不干扰。

用一句话总结这页的核心

“用继承来表达‘能力’,是 C++ 早期设计的历史包袱;
Rust Traits(以及你刚写的 type-erasure RP)把‘能力’从继承中解放了出来。”

#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <typeindex>
/* =========================================
   1. 序列化接口(你给的原样)
   ========================================= */
struct ISerialize {
    virtual std::ostream&
    serialize(std::ostream&) const = 0;
    virtual std::size_t
    length() const = 0;
    virtual ~ISerialize() {}
};
/* =========================================
   2. 一个极简 type-id registry
   ========================================= */
struct TypeRegistry {
    template<typename T>
    std::string id() const {
        static const std::string name =
            std::to_string(next_id_++);
        return name;
    }
private:
    inline static int next_id_ = 1;
};
inline TypeRegistry g_registry;
/* =========================================
   3. 通用模板 Wrapper
   ========================================= */
template<typename T>
struct SerializeWrapper : ISerialize {
    T value_;
    explicit SerializeWrapper(const T& v)
        : value_(v)
    {}
    std::ostream&
    serialize(std::ostream& to) const override {
        return to << g_registry.id<T>() << ':' << value_;
    }
    std::size_t
    length() const override {
        std::ostringstream temporary;
        serialize(temporary);
        return temporary.str().length();
    }
};
/* =========================================
   4. 特化 / 手写 wrapper(int)
   ========================================= */
struct SerializeWrapperInt : ISerialize {
    int value;
    explicit SerializeWrapperInt(int v)
        : value(v)
    {}
    std::ostream&
    serialize(std::ostream& to) const override {
        return to << "int:" << value;
    }
    std::size_t
    length() const override {
        std::ostringstream tmp;
        tmp << "int:" << value;
        return tmp.str().length();
    }
};
/* =========================================
   5. 使用示例
   ========================================= */
int main() {
    SerializeWrapper<int>        wi{42};
    SerializeWrapper<std::string> ws{"hello"};
    SerializeWrapperInt          wi2{123};
    ISerialize* objects[] = {
        &wi,
        &ws,
        &wi2
    };
    for (const ISerialize* obj : objects) {
        std::ostringstream out;
        obj->serialize(out);
        std::cout
            << out.str()
            << " (length=" << obj->length() << ")\n";
    }
}
1:42 (length=4)
2:hello (length=7)
int:123 (length=7)

39⃣ 悲剧在哪里?Subtyping ≠ Subclassing

The tragedy is that subtyping-as-subclassing is the most popular and the worst way for doing subtyping

解释

悲剧在于:
把「子类型(subtyping)」等同于「继承(subclassing)」
既是最流行的做法,也是最糟糕的做法。

在传统 C++ / Java / C# 中:

A 是 B 的子类型  ⇔  A 继承自 B

也就是把语义关系硬性绑定为语法结构
用一个形式化描述:

子类型关系被定义为
A < : B    ⟺    A  extends  B A <: B \iff A \text{ extends } B A<:BA extends B
这一步在类型论意义上是错误的,因为:

  • 子类型是能否安全替代的问题
  • 继承是对象内存结构 / 生命周期 / 身份的问题
    它们不是同一个维度。

为什么这是“最糟糕”的?

因为继承在 C++ 里意味着:

  • 对象布局被固定
  • 析构、构造语义被耦合
  • 只能继承一个主基类
  • 类型必须在定义时参与关系
    但子类型真正想表达的是:

“你能不能对我调用某组操作?”

40⃣ Rust 知道得更清楚(Rust knows better)

Rust 明确拒绝把子类型关系建模为继承层级。
Rust 的核心思想是:

子类型 = 行为能力(capability)
而不是对象结构(structure)

41⃣ Rust Trait:子类型关系的定义

// Definition of the subtyping relation:
// you can invoke serialize(object, output)
// on anything that implements this trait.
pub trait Serialize {
    fn serialize(&self, to: &mut dyn io::Write) -> io::Result<()>;
}

逐行解释

pub trait Serialize {
  • trait 定义的是一种 能力 / 行为契约
  • 它不是类
  • 不会产生对象层级
  • 不影响内存布局
fn serialize(&self, to: &mut dyn io::Write) -> io::Result<()>;

这行代码本身就是对子类型关系的定义

如果一个类型 T T T 实现了 Serialize
那么我们就可以安全地对它调用:

s e r i a l i z e ( T , output ) serialize(T, \text{output}) serialize(T,output)
也就是说:
T < : S e r i a l i z e    ⟺    T  提供  s e r i a l i z e ( s e l f , W r i t e ) T <: Serialize \iff T \text{ 提供 } serialize(self, Write) T<:SerializeT 提供 serialize(self,Write)
注意
这里的 < < <: 是语义子类型,不是继承。

42⃣ 为内建类型 i32 实现 Trait(关键点)

impl Serialize for i32 {
    fn serialize(&self, to: &mut dyn io::Write) -> io::Result<()> {
        write!(to, "{}", self)
    }
}

这里发生了什么(非常关键)

  • i32内建类型
  • 你没有修改 i32 的定义
  • 你只是 在当前 crate 中声明一种能力绑定

在 C++ 里这是不可能的
因为 C++ 的接口必须通过继承获得

对比 C++

在 C++ 中你做不到

struct int : ISerialize { ... }; // 不可能

于是你只能:

  • wrapper
  • adapter
  • proxy
    而 Rust 允许:
impl Trait for Type

这一步在类型论中叫:

retroactive interface implementation
(事后接口实现)

43⃣ 用户自定义类型也一样

pub struct Point {
    x: f64,
    y: f64,
}

这是一个普通数据类型,没有任何“继承负担”。

impl Serialize for Point {
    fn serialize(&self, to: &mut dyn io::Write) -> io::Result<()> {
        write!(to, "({}, {})", self.x, self.y)
    }
}

关键理解

  • Point 不是 Serialize 的子类
  • 它只是 选择参与某个子类型关系
  • 这个决定:
    • 可逆
    • 可并存
    • 不影响其它 trait

44⃣ 这一步在 C++ 中为什么是结构性不可能?

你那一页说得非常准确:

primitive types like int can’t implement interfaces,
thus we must use wrappers.
原因不是 C++ “不够强”,而是:
C++ 把“子类型”绑定为“对象继承”
即:
T < : I ⇒ T  必须继承  I T <: I \Rightarrow T \text{ 必须继承 } I T<:IT 必须继承 I
而 Rust 的规则是:
T < : T r a i t    ⟺    T  实现 Trait 的方法集合 T <: Trait \iff T \text{ 实现 Trait 的方法集合} T<:TraitT 实现 Trait 的方法集合
没有对象层级,没有内存耦合。

45⃣ Rust Traits 的本质总结(你这页的 bullet)

Rust Traits

• 一种 opt-in 机制,让类型选择是否参与某种子类型关系
• 与继承无关,不需要基类
• 本文只讨论 runtime polymorphism,忽略编译期 trait bounds
• 类似 Python 的 Duck Typing,但绑定是显式的、可检查的

用一句话收尾(非常重要)

Rust 的 Trait 把“我是谁(what I am)”
和“我能做什么(what I can do)”彻底分离了。

use std::io::{self, Write};
/* =========================================
   1. 定义子类型关系(Trait)
   ========================================= */
/// Serialize 定义了一种“能力”:
/// 任何实现了该 trait 的类型,都可以被序列化到 io::Write。
pub trait Serialize {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()>;
}
/* =========================================
   2. 为内建类型实现 Trait
   ========================================= */
impl Serialize for i32 {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "i32:{}", self)
    }
}
impl Serialize for String {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "string:\"{}\"", self)
    }
}
/* =========================================
   3. 用户自定义类型
   ========================================= */
struct Point {
    x: f64,
    y: f64,
}
/* 让 Point 参与 Serialize 子类型关系 */
impl Serialize for Point {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "Point({}, {})", self.x, self.y)
    }
}
/* =========================================
   4. 使用 Trait Object 做运行期多态
   ========================================= */
fn dump_all(objects: &[&dyn Serialize]) -> io::Result<()> {
    let mut out = io::stdout();
    for obj in objects {
        obj.serialize(&mut out)?;
        writeln!(out)?;
    }
    Ok(())
}
/* =========================================
   5. 主函数
   ========================================= */
fn main() -> io::Result<()> {
    let n: i32 = 42;
    let s = String::from("hello");
    let p = Point { x: 1.5, y: 2.5 };
    // 不同类型,统一视为 &dyn Serialize
    let values: Vec<&dyn Serialize> = vec![&n, &s, &p];
    dump_all(&values)
}
use std::io::{self, Write};
// 引入标准库中的 IO 模块
// io        : 提供 Result、stdout 等 IO 基础设施
// Write     : 一个 trait,表示“可以被写入字节流的对象”
//
// dyn Write 表示“某个在运行期才知道具体类型的、实现了 Write 的对象”
// 类似于 C++ 里的 std::ostream& 或基类指针
/* =========================================
   1. 定义子类型关系(Trait)
   ========================================= */
/// Serialize 定义了一种“能力”
///
/// 注意:
/// - 这不是类
/// - 不涉及继承
/// - 不影响任何类型的内存布局
///
/// 语义含义是:
/// “任何实现了 Serialize 的类型,都可以被当作 Serialize 使用”
pub trait Serialize {
    /// serialize 方法本身,就是对子类型关系的定义
    ///
    /// 如果某个类型 T 实现了这个方法:
    ///     fn serialize(&T, &mut dyn Write) -> io::Result<()>
    ///
    /// 那么我们就认为:
    ///     T <: Serialize
    ///
    /// &self:
    ///     只读借用,不获取所有权
    ///
    /// &mut dyn Write:
    ///     一个 trait object
    ///     表示“任何实现了 Write 的输出目标”
    ///
    /// io::Result<()>:
    ///     明确表示这是一个可能失败的 IO 操作
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()>;
}
/* =========================================
   2. 为内建类型实现 Trait
   ========================================= */
// 为 Rust 的内建整数类型 i32 实现 Serialize
//
// 关键点:
// - i32 是 primitive type
// - 我们没有、也不可能修改 i32 的定义
// - 这里做的只是:
//     “声明 i32 具备 Serialize 这一能力”
//
// 在 C++ 中这是结构性不可能的(不能让 int 继承接口)
impl Serialize for i32 {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        // write! 是一个宏
        // 它会把格式化后的字符串写入到 to 中
        //
        // 这里的 self 是 &i32
        // 并没有发生任何对象包装或拷贝
        write!(to, "i32:{}", self)
    }
}
// 为标准库类型 String 实现 Serialize
//
// 再次注意:
// - String 也不是我们定义的
// - 但我们依然可以为它“外挂”一个能力
impl Serialize for String {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "string:\"{}\"", self)
    }
}
/* =========================================
   3. 用户自定义类型
   ========================================= */
// 一个普通的用户自定义数据类型
//
// 没有继承
// 没有实现任何 trait
// 只是一个纯数据结构
struct Point {
    x: f64,
    y: f64,
}
// 让 Point “选择性地”参与 Serialize 这一子类型关系
//
// 这一步是 opt-in 的:
// - Point 本身并不“是” Serialize
// - 它只是声明:
//     “我可以被当作 Serialize 使用”
impl Serialize for Point {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "Point({}, {})", self.x, self.y)
    }
}
/* =========================================
   4. 使用 Trait Object 做运行期多态
   ========================================= */
// dump_all 接受一个切片:
///     &[&dyn Serialize]
//
// 含义是:
// - 这是一个“引用的集合”
/// - 每个引用都指向某个实现了 Serialize 的对象
// - 这些对象的具体类型在编译期可以不同
//
// &dyn Serialize 在语义上等价于:
//     C++ 的 ISerialize*
// 但:
// - 没有继承
// - 没有基类对象
// - 只是 (data_ptr, vtable_ptr)
fn dump_all(objects: &[&dyn Serialize]) -> io::Result<()> {
    // 获取标准输出
    // stdout() 返回一个具体类型 Stdout
    // 但我们后面只通过 Write trait 使用它
    let mut out = io::stdout();
    for obj in objects {
        // 这里发生的是:运行期动态分发
        //
        // Rust 会:
        // - 通过 obj 中的 vtable
        // - 找到对应类型的 serialize 实现
        // - 调用正确的函数
        obj.serialize(&mut out)?;
        // 每个对象序列化完后换行
        writeln!(out)?;
    }
    Ok(())
}
/* =========================================
   5. 主函数
   ========================================= */
fn main() -> io::Result<()> {
    // 三个完全不同的类型
    let n: i32 = 42;
    let s = String::from("hello");
    let p = Point { x: 1.5, y: 2.5 };
    // 把不同类型的引用
    // 统一“上转型”为 &dyn Serialize
    //
    // 这里发生的是:
    // - 自动构造 trait object
    // - 保存 (对象地址, 对应的 vtable)
    let values: Vec<&dyn Serialize> = vec![&n, &s, &p];
    // 通过统一接口进行运行期多态调用
    dump_all(&values)
}

用一句“讲义级总结”收尾

在 Rust 中:
子类型不是“我继承了谁”,
而是“我承诺了哪些行为”。

44⃣ Now, a C++ world of hurt

现在,进入 C++ 的痛苦世界
这句话不是情绪化吐槽,而是技术上的判决
一旦你坚持用“继承 = 子类型”,痛苦是结构性的,不是写法问题

✗ 示例:把接口塞进成员变量

struct UserType {
    // ...
    ISerialize instanceMemberVariable_;
    // ...
};

理解

很多人看到 “不能继承?那我就当成员变量!”
这是 C++ 世界里一个非常常见、也非常危险的“补救直觉”。

这里的问题有两个层次:

1⃣ 这是对象切片(Object Slicing)

Slicing!
如果你写过:

SerializeWrapperInt w{42};
ISerialize base = w;   // ✗ 切片

那么:

  • SerializeWrapperInt 的派生部分被直接砍掉
  • base 只剩下一个“抽象基类外壳”
  • 虚函数行为 直接丢失
    形式化一点说:

你以为在做:
Derived → Base \text{Derived} \rightarrow \text{Base} DerivedBase

实际发生的是:
truncate ( Derived ) \text{truncate}(\text{Derived}) truncate(Derived)

2⃣ 你根本不能把抽象类作为成员

ISerialize instanceMemberVariable_; // ✗ 编译都过不了

原因是:

  • ISerialize 有纯虚函数
  • 抽象类型不能被实例化
  • 成员变量意味着“值语义存储”
    这一步直接暴露了:
    继承接口 ≠ 可组合能力

45⃣ Slicing:不是 bug,是模型必然结果

对象切片不是“你写错了”,而是:

C++ 的值语义 + 继承层级
在语义上是冲突的
在 C++ 中:

  • 值语义 ⇒ 对象完整拷贝
  • 继承 ⇒ 对象前缀布局
    当你写:
ISerialize x = derived;

你在要求系统同时满足:
拷贝完整对象 ∧ 只保留基类部分 \text{拷贝完整对象} \land \text{只保留基类部分} 拷贝完整对象只保留基类部分
这是自相矛盾的要求

46⃣ Inheritance Intrusiveness(继承的侵入性)

为了 runtime polymorphism,强行施加结构性约束
你这一页列的 bullet,其实是在说:
继承不是“轻量语义”,而是“重型结构承诺”

被迫付出的 5 个成本(逐条解释)

1⃣ Allocation(分配)

  • 多态对象往往必须 heap 分配
  • 因为你只能通过指针 / 引用使用它
  • 小对象优化直接失效

2⃣ Indirection(间接访问)

  • 每次调用都要:
    • 指针跳转
    • vtable 跳转
  • cache locality 明显变差

3⃣ Lifetime Management(生命周期管理)

  • 谁 new?
  • 谁 delete?
  • 用 raw pointer 还是 smart pointer?
  • shared_ptr 还是 unique_ptr?
    这些问题与你的业务逻辑无关,却被强制引入。

4⃣ Incentive to share state(鼓励共享状态)

  • 基类里很容易放 protected 成员
  • 派生类开始依赖隐式共享状态
  • 形成脆弱基类问题

5⃣ Disables local reasoning(破坏局部推理)

  • 你看到一个函数:
    void f(ISerialize& s);
    
  • 你根本不知道:
    • 实际类型是什么
    • 会不会改内部状态
    • 是否有隐藏副作用
      代码阅读成本指数级上升。

Referential Semantics(被迫使用引用语义)

你这页提到的这一点非常关键:

Requires referential semantics
因为:

  • 抽象基类不能按值传递
  • 只能用:
    • ISerialize*
    • ISerialize&
    • unique_ptr<ISerialize>
      这直接导致:
      逻辑上是值 ⇒ 实现上必须是引用 \text{逻辑上是值} \Rightarrow \text{实现上必须是引用} 逻辑上是值实现上必须是引用
      这是语义错位

47⃣ Subclassing Pains(继承的系统性疼痛)

这一页是现实工程的总结性控诉

1⃣ Intrusive:侵入性极强

我们必须为了进入继承体系,去 wrap 本来完全好的类型

struct SerializeWrapperInt : ISerialize {
    int value;
};
  • int 本来没有任何问题
  • wrapper 完全是“为了接口而接口”
    这就是 busy work(纯体力活)

2⃣ 序列化需求通常是“事后出现的”

Frequently the need to serialize a type is identified long after writing the type
现实是:

  • 你先写业务类型
  • 几个月后:
    • 想存文件
    • 想发网络
    • 想打日志
      然后你发现:

类型已经“冻结”在某个继承结构里了

3⃣ 不同应用想要不同序列化方式

例如:

  • App A:JSON
  • App B:Binary
  • App C:Protobuf
    如果你走继承路线:
struct MyType : IJsonSerialize { ... }

后来你想加:

struct MyType : IJsonSerialize, IProtobufSerialize // ✗

你会立刻遇到:

  • 多继承复杂性
  • 菱形继承
  • 接口冲突
  • ABI 风险
    这是“雪崩式工作量”的起点。

4⃣ Take it or leave it(语言层面的死规矩)

A feature of the language you can’t finesse
这是最残酷的一点:

  • 你不能“稍微用好一点的继承”
  • 这是语言层级的建模选择
  • 没有技巧可以绕开

5⃣ Further problems(以及更多…)

这里省略号的含义是:

  • ABI 脆弱
  • 编译依赖放大
  • ODR 风险
  • 单元测试困难
  • Mock 极其麻烦

一句话的“讲义级总结”

在 C++ 中,
一旦你用继承来表达“能力”,
你就已经输了——
后面的每一步,只是在止血。

这也正是为什么你前面引出:

  • Rust Traits
  • type-erasure RP
  • non-intrusive capability binding

一、Rust 版本:把“子类型关系”放进字段里(Trait Object)

1⃣ UserType:字段是 Box<dyn Serialize>

pub struct UserType {
    // dyn Serialize = trait object(特征对象)
    //
    // 本质是一个“胖指针(fat pointer)”:
    //   1. 数据指针:指向真实的值(Point / i32 / String / ...)
    //   2. vtable 指针:指向该具体类型对应的 Serialize 虚函数表
    //
    // dyn Serialize 是 unsized(大小在编译期未知),
    // 所以它**不能直接作为字段存在**,必须放在指针后面。
    //
    // Box<T>:
    //   - 在堆上分配
    //   - 拥有所有权
    //   - 生命周期清晰(跟着 UserType 走)
    member: Box<dyn Serialize>,
}

核心思想

  • UserType 并不“继承 Serialize”
  • 它只是**“拥有一个实现了 Serialize 的东西”**
  • 这叫 组合(composition)而不是继承(inheritance)
    这一步,Rust 已经彻底绕开了 C++ 的 subtyping-as-subclassing

二、构造函数:接受“任何实现了 Serialize 的类型”

impl UserType {
    pub fn new<T>(x: T) -> Self
    where
        // T 必须实现 Serialize
        // 'static:T 不能借用短生命周期的引用
        //
        // 为什么需要 'static?
        // 因为:
        //   Box<dyn Serialize> 没有生命周期参数,
        //   编译器必须保证里面的值可以“活得足够久”
        T: Serialize + 'static,
    {
        // Box::new(x)
        //   1. 在堆上分配 x
        //   2. 发生一次“unsize coercion”:
        //      Box<T> → Box<dyn Serialize>
        //   3. 编译器生成 T 对应的 Serialize vtable
        Self {
            member: Box::new(x),
        }
    }

对比 C++


C++ Rust
ISerialize* / unique_ptr<ISerialize> Box<dyn Serialize>
手写虚函数 trait + impl
容易 slicing 完全不可能 slicing
生命周期靠约定 生命周期由类型系统保证

三、运行期“换模型”:set()

    pub fn set<T>(&mut self, x: T)
    where
        T: Serialize + 'static,
    {
        // 旧的 Box<dyn Serialize> 被 drop
        // 新的值被 move 进来
        //
        // 没有共享状态
        // 没有悬空指针
        // 没有 use-after-free
        self.member = Box::new(x);
    }
}

关键点(非常 Rust)

  • move 是默认语义
  • 旧值被安全销毁(Drop)
  • 不可能有人还在偷偷用旧对象
    这正是 Rust 所谓:

Make invalid states unrepresentable

四、使用示例:运行期多态,但没有继承地狱

let mut buf = Vec::new();
let mut u = UserType::new(Point { x: 1.0, y: 2.0 });
u.serialize_to(&mut buf)?;   // 使用 Point 的实现
u.set(7_i32);                // 运行期替换成 i32
u.serialize_to(&mut buf)?;   // 使用 i32 的实现

输出语义上等价于:

(1, 2)
7

但注意:

  • UserType 本身完全不知道具体类型
  • 只依赖 Serialize 这个“能力”

五、为什么 C++ 会“痛不欲生”(你前面那几页)

1⃣ 对象切片(Slicing)

struct UserType {
    ISerialize instanceMemberVariable_; // ✗
};

问题:

  • ISerialize 是基类
  • 派生类信息被直接切掉
  • 虚函数表都没了
    Rust:trait object 永远是“指向完整对象”的指针

2⃣ 继承的侵入性(Inheritance Intrusiveness)

你列的那一堆痛点,逐条对应:

C++ 继承 Rust trait
强制 heap / pointer 是否分配由你决定
强制间接调用 trait object 才是动态分发
生命周期模糊 所有权模型清晰
容易共享状态 默认 move,不共享
禁止局部推理 借用规则强制局部推理

一句话总结:

C++ 把“运行期多态”绑定在“对象布局”上
Rust 把“运行期多态”绑定在“行为(trait)”上

3⃣ “事后想加接口”的灾难

C++:

  • int 不能继承 ISerialize
  • 只能包一层 SerializeInt
  • 过几年想换序列化格式?
  • 再包一层
  • 再多继承
  • 接口雪崩
    Rust:
impl Serialize for i32 {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "{}", self)
    }
}

✓ 不侵入
✓ 可追加
✓ 可并存多个 trait

六、“In the Style of Rust”:思想层面的差异

1⃣ 可变性是被“闸门”控制的

Rust 的核心规则之一:

同一时刻:

  • 要么有 一个 &mut T
  • 要么有 多个 &T
    可以写成逻辑约束(非数学意义):
    ¬ ( ∃ , & m u t T ∧ ∃ , & T ) \neg(\exists, \&mut T \land \exists, \&T) ¬(,&mutT,&T)
    这在 编译期 被强制执行。

2⃣ Move 是默认,Copy 是特权

  • C++:复制是默认,危险是隐形的
  • Rust:
    • Copy:像 i32 这样的平凡类型
    • Clone:显式、可见、有成本
      结果:

use-after-move、悬空引用、别名修改

在类型系统层面被禁止

七、最后一页:Rust 的“唯一大限制”

你引用的这段话其实非常诚实:

我们没法在 C++ 里表达 Rust 的借用规则
所以现实只有两条路:

  1. 继续 C++,继续自由,但接受风险
  2. 学习 Rust 的风格,把约束变成自律
    Rust 的贡献不是“语法”,而是:

把正确性规则变成“无法违反的事实”

八、一句话总总结(送你一句很 Rust 的话)

C++:你可以做任何事,包括错事
Rust:你只能做对的事,除非你明确说“unsafe”
场景目标

  • 定义一个 Serialize trait(子类型关系)
  • 内建类型和用户类型都能“事后加入”
  • 使用 trait object (dyn Serialize)运行期多态
  • 不用继承、不用 wrapper、不存在 slicing
  • 可以在运行期 替换具体实现

✓ 完整可运行示例(main.rs

use std::io::{self, Write};
/* =========================================================
   1. 定义“子类型关系”:Serialize trait
   ========================================================= */
/// Serialize 描述的是一种“能力”,而不是一种“类型层级”
///
/// 只要一个类型实现了 Serialize:
///   - 我们就可以在运行期
///   - 通过 &dyn Serialize
///   - 调用 serialize(),而不关心它的具体类型
///
/// 这就是 Rust 的“subtyping without inheritance”
pub trait Serialize {
    /// 将自身序列化到任意实现了 Write 的输出中
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()>;
}
/* =========================================================
   2. 为内建类型实现 Serialize(C++ 做不到的地方)
   ========================================================= */
/// i32 是语言内建类型
/// 在 C++ 中:
///   - 不能让 int 继承 ISerialize
///   - 只能写 wrapper
///
/// 在 Rust 中:
///   - 直接为 i32 绑定 Serialize
///   - 不侵入、不修改定义、不破坏封装
impl Serialize for i32 {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "i32:{}", self)
    }
}
/// String 同理
impl Serialize for String {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "string:\"{}\"", self)
    }
}
/* =========================================================
   3. 用户自定义类型
   ========================================================= */
/// 一个普通的用户类型
/// 没有继承任何东西
/// 完全不知道 Serialize 的存在
struct Point {
    x: f64,
    y: f64,
}
/// “事后”把 Point 接入 Serialize 这条子类型关系
impl Serialize for Point {
    fn serialize(&self, to: &mut dyn Write) -> io::Result<()> {
        write!(to, "Point({}, {})", self.x, self.y)
    }
}
/* =========================================================
   4. 在结构体中存放 trait object(运行期多态)
   ========================================================= */
/// UserType 并不实现 Serialize
/// 它只是“拥有一个可以 Serialize 的东西”
pub struct UserType {
    // dyn Serialize = trait object(特征对象)
    //
    // 本质是一个“胖指针(fat pointer)”:
    //   - 数据指针:指向真实对象
    //   - vtable 指针:指向该类型的 Serialize 实现
    //
    // dyn Serialize 是 unsized(编译期大小未知)
    // → 必须放在指针后面
    //
    // Box:
    //   - 堆分配
    //   - 独占所有权
    //   - 生命周期清晰
    member: Box<dyn Serialize>,
}
impl UserType {
    /// 构造函数:接受任何实现了 Serialize 的类型
    pub fn new<T>(x: T) -> Self
    where
        // T 必须实现 Serialize
        // 'static 保证:
        //   - T 不借用短生命周期的引用
        //   - 可以安全放进 Box<dyn Serialize>
        T: Serialize + 'static,
    {
        Self {
            // Box::new(x) 后发生 unsize coercion:
            //   Box<T> → Box<dyn Serialize>
            member: Box::new(x),
        }
    }
    /// 运行期替换内部对象
    pub fn set<T>(&mut self, x: T)
    where
        T: Serialize + 'static,
    {
        // 旧值被 drop
        // 新值被 move 进来
        //
        // 无共享状态
        // 无悬空指针
        // 无 slicing
        self.member = Box::new(x);
    }
    /// 对外暴露的统一行为
    pub fn serialize_to(&self, to: &mut dyn Write) -> io::Result<()> {
        self.member.serialize(to)
    }
}
/* =========================================================
   5. 使用 trait object 切片做运行期多态
   ========================================================= */
/// 接受“任何实现了 Serialize 的东西”
///
/// &[&dyn Serialize] =
///   - 不同具体类型
///   - 统一通过 vtable 调用
fn dump_all(values: &[&dyn Serialize]) -> io::Result<()> {
    let mut out = io::stdout();
    for v in values {
        v.serialize(&mut out)?;
        writeln!(out)?;
    }
    Ok(())
}
/* =========================================================
   6. main:完整演示
   ========================================================= */
fn main() -> io::Result<()> {
    // 不同的具体类型
    let n: i32 = 42;
    let s = String::from("hello");
    let p = Point { x: 1.5, y: 2.5 };
    // 统一视为 &dyn Serialize(运行期多态)
    let values: Vec<&dyn Serialize> = vec![&n, &s, &p];
    dump_all(&values)?;
    // -----------------------------------------------------
    // 结构体里存 trait object
    let mut buf = Vec::new();
    let mut u = UserType::new(Point { x: 1.0, y: 2.0 });
    u.serialize_to(&mut buf)?;
    buf.push(b'\n');
    // 运行期替换为完全不同的类型
    u.set(7_i32);
    u.serialize_to(&mut buf)?;
    println!("\n--- UserType buffer ---");
    println!("{}", String::from_utf8_lossy(&buf));
    Ok(())
}

这一个例子解决了哪些 C++ 痛点?


C++ 问题 Rust 这里怎么解决
slicing trait object 永远是指针
必须继承 trait 是“能力”,不是“层级”
不能给 int 加接口 impl Serialize for i32
wrapper 爆炸 不需要 wrapper
生命周期不清晰 所有权 + 'static
隐式 copy move 默认、clone 显式

一句话总结

Rust 把“运行期多态”从“对象布局问题”
变成了“行为契约问题”


https://wandbox.org/permlink/bvZXSQy7BA6hjn1Y

一、先从一句关键结论开始

By the way, the struct UserType is move-only

这句话为什么重要?

回忆 Rust 里的代码:

pub struct UserType {
    member: Box<dyn Serialize>,
}

在 Rust 里:

  • Box<T> 不实现 Copy
  • 移动(move)是默认语义
  • 复制(clone)是显式选择
    所以:
let a = u;
let b = u; // ✗ 编译错误:use after move

UserType 天生是 move-only 的
这是语言级别强制的设计纪律,不是库作者的“约定”。

二、为什么 C++ 世界“很痛”(Now, a C++ world of hurt)

1⃣ 对象切片(Slicing)

struct UserType {
    ISerialize instanceMemberVariable_;
};

问题在哪?

  • ISerialize 是一个 抽象基类
  • 成员变量是 按值存储
    当你试图:
UserType u;
u.instanceMemberVariable_ = SerializeWrapperInt{42};

发生的事情是:

  • 派生类部分被切掉
  • 只剩下 ISerialize 子对象
  • 动态行为丢失
    这就是 Object Slicing
    Rust 里根本不存在这种情况,因为:
  • trait object 永远是 胖指针
  • 永远不会按值复制 trait object

2⃣ 继承的“侵入性”(Inheritance Intrusiveness)

列了一堆 bullet points,看起来乱,其实可以总结为一句话:

为了 runtime polymorphism,你被迫改变对象的“物理形态”

具体痛点逐条成人话

(1)Allocation(分配)

  • 想要多态 → 通常要 new
  • 生命周期变复杂

(2)Indirection(间接层)

  • 虚函数调用
  • 指针跳转
  • cache 不友好

(3)Lifetime management(生命周期)

  • 谁 delete?
  • shared_ptr / unique_ptr 地狱

(4)Incentive to share state(鼓励共享状态)

  • “为了避免拷贝,大家都用指针吧”
  • aliasing bug 温床

(5)Disables local reasoning(破坏局部推理)

你看到一个对象:

ISerialize* p;

完全不知道

  • 它是谁分配的
  • 能不能拷贝
  • 能不能 move
  • 析构是否安全

3⃣ Subclassing 的结构性失败

痛点总结(第 47 页)

✗ 侵入式(Intrusive)
  • 原本好好的类型
  • 只为了 RP
  • 被迫:
    • 继承接口
    • 或写 wrapper
✗ 事后需求(Retrofit)

“我们写完类型半年后,才发现需要 serialize”
在 C++ 里:

  • 原类型不能改
  • 只能 wrapper
  • wrapper 爆炸
✗ 多种序列化方式
  • JSON
  • Binary
  • Protobuf
    你会被迫:
struct MyType : ISerialize, IJsonSerialize, IProtoSerialize;

这是语言层面的死路

三、Part 2:Type Erasure = External Polymorphism of Ownership

这是整场 talk 的理论核心

1⃣ 什么是 External Polymorphism?

多态不在类型内部,而在类型外部表达
对比:


方式 多态在哪里
继承 类型内部(class 层级)
Rust trait 外部 impl
Type Erasure 外部 vtable

2⃣ 什么是 Type Erasure?

如果你把“销毁 / move / copy / 行为”
都放到外部 vtable 管理,
那你就“抹掉了类型”

形式化一点:
对象 =
storage + vtable \text{storage} + \text{vtable} storage+vtable

  • storage:保存真实对象
  • vtable:保存行为(函数指针)

3⃣ 为什么叫“internal external polymorphism”

听起来矛盾,其实不矛盾:

  • external
    • 原类型不知道多态
    • 不继承、不侵入
  • internal
    • 对使用者来说
    • 它“看起来像一个对象”

四、Zoo Type Erasure 在干什么?

Zoo(zoo::tea)的目标是:

把 C++ 的 Type Erasure 做到:

  • 高性能
  • 可审计
  • 可组合
  • 类似 Rust Traits

1⃣ 用户视角

using Policy =
    zoo::Policy<
        void *,               // 本地存储策略
        zoo::Destroy,
        zoo::Move,
        zoo::Copy,
        SerializeAffordance   // 用户定义的 trait
    >;
using Serializable = zoo::AnyContainer<Policy>;

这行代码在“声明什么”?
“Serializable 拥有哪些运行期能力”

  • Destroy:怎么析构
  • Move:怎么 move
  • Copy:怎么 copy
  • SerializeAffordance:怎么 serialize

2⃣ SerializeAffordance = “C++ 版 trait”

struct SerializeAffordance {
    struct VTableEntry {
        std::OSTREAM *(*serialize_impl)(std::OSTREAM &, const void *);
        std::size_t (*length_impl)(const void *);
    };

对应 Rust:

trait Serialize {
    fn serialize(&self, to: &mut Write);
    fn length(&self) -> usize;
}

一一对应,只是手写了 vtable。

3⃣ 为任意类型生成 vtable(核心魔法)

template<typename ConcreteValueManager>
constexpr static inline VTableEntry Operation = {
    [](std::OSTREAM &to, const void *cvm) {
        using OriginalType =
            typename ConcreteValueManager::ManagedType;
        const OriginalType *value =
            static_cast<const ConcreteValueManager *>(cvm)->value();
        return impl::howToSerializeT(to, *value);
    },
    [](const void *cvm) {
        std::OSTRINGSTREAM tmp;
        Operation<ConcreteValueManager>.serialize_impl(tmp, cvm);
        return tmp.str().length();
    }
};

关键点

  • ConcreteValueManager 知道真实类型
  • vtable 里只有 void*
  • 类型信息在生成 vtable 时被“抹掉”
    这就是 Type Erasure

4⃣ 动态派发(Dynamic Dispatch)

auto implementation =
    baseValueManagerPtr
        ->template vTable<SerializeAffordance>()
        ->serialize_impl;
return *implementation(out, baseValueManagerPtr);

这就是:

obj.serialize(out)

只是 Rust 帮你写好了。

五、与 Rust Trait 的最终对齐

Rust 可以这样写:

impl<T: Display> Serialize for T {
    fn serialize(&self, to: &mut dyn Write) {
        write!(to, "{}", self)
    }
}

Zoo / C++ 这边:

  • 模板 + 约束
  • 自动为“满足条件的类型”生成 affordance
    语义完全等价

六、终极总结(Recap,用人话)

Rust Traits 给了你什么?

  • 子类型关系 ≠ 继承
  • opt-in
  • 运行期多态不破坏类型设计
  • 编译器强制内存与别名安全

C++ Type Erasure(Zoo)给了你什么?

  • 同样的表达能力
  • 更自由的存储策略(不一定要 heap)
  • 可控性能
  • 仍然是 C++

唯一缺的是什么?

借用检查器(borrow checker)
所以最后一句话才是点睛:
我们无法在 C++ 中表达 Rust 的规则,
只能选择:
要么继续为所欲为,
要么自律,向 Rust 的风格学习。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐