CppCon 2025 学习:Rust Traits in Style for C++ How We Unlocked Their Big Benefits for Users and Much
也就是:传统 RP 的问题必须用继承必须有虚表(vptr)必须通过指针 / 引用使用对象语义丢失很难组合(composition)性能不可控(间接调用)ABI / ODR / 代码膨胀问题Rust 社区对此的回应是:Traits二、这段话在说什么(逐条 + 解读)1⃣不用不用继承体系2⃣3⃣Rust Trait 特性C++ Type Erasure无继承层级无继承行为约束行为约束可静态 / 动态可
一、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 的问题
- 必须用继承
- 必须有虚表(vptr)
- 必须通过指针 / 引用使用
- 对象语义丢失
- 很难组合(composition)
- 性能不可控(间接调用)
- 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 在编译期生成
✓ 所有同类型对象共享这张表
这正是传统virtualvtable 的等价物。
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 件关键事情:
- 把具体对象堆分配
→ 保存为object_ = new T(...)void* - 记录“行为表”
→ 这是&DrawableModule<T>::vtableT的“能力说明书” - 记录正确的删除方式
delete_impl<T> - 记录正确的拷贝方式
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(里氏替换原则)
这是“多态”在理论上的定义
原意(简化版)
如果 S 是 T 的一种实现:
那么所有使用
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
既然要包很多类型:
intdoublestd::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
intstd::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<:B⟺A 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<:Serialize⟺T 提供 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<:I⇒T 必须继承 I
而 Rust 的规则是:
T < : T r a i t ⟺ T 实现 Trait 的方法集合 T <: Trait \iff T \text{ 实现 Trait 的方法集合} T<:Trait⟺T 实现 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} Derived→Base实际发生的是:
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 的借用规则
所以现实只有两条路:
- 继续 C++,继续自由,但接受风险
- 学习 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
UserTypeis 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 的风格学习。
更多推荐



所有评论(0)