[Panyim] C++ 比 C 更好吗

------ 关于语言设计权衡的讨论

探索C语言动画引擎迁移C++可行性
[一、Panyim 的任务系统架构](#一、Panyim 的任务系统架构)
二、为什么需要自定义虚表
[三、从 C 迁移到 C++](#三、从 C 迁移到 C++)
[四、现代 C++](#四、现代 C++)
[五、 C 是 C++ 的子集为误解](#五、 C 是 C++ 的子集为误解)
附录

探索将一个现有的 C 语言实现的编程动画引擎迁移到 C++ 的可行性

发现 C++ 的虚函数并不能解决实际存在的问题


一、Panyim 的任务系统架构

现有一套基于 Task(任务) 的动画描述系统:

复制代码
单个根任务 → 控制整个动画流程

整个动画由一个单一的根任务驱动,这个任务内部又由复合任务(Compound Tasks)组成

1.2 任务类型

实现了几种基本任务类型:

任务 作用 示例
Wait 等待指定时间 wait_ms(750)
Write 向单元格写入符号 写入文字或图片
Sequence 顺序执行多个任务 播放 intro → 写符号1 → 移动 → 写符号2...
Group 同时执行多个任务 Windows 加载动画(三个方块同时移动)

1.3 Group 任务的实现

Windows 加载动画的实现方式:

  • 定义一个 shuffle 函数,产生一次方块移动
  • 三个 shuffle 组成一个 Group,三个方块同时移动
  • 三个 Group 组成 Sequence,形成完整的加载动画
c 复制代码
// 三个移动同时发生
TaskGroup {
    Move(square1, target1),
    Move(square2, target2),
    Move(square3, target3)
}

二、为什么需要自定义虚表

2.1 问题:热重载与 DLL

Panyim 的插件系统基于 DLL(动态链接库)

  • 自定义动画以 DLL 形式存在
  • 插件状态在 DLL 重载之间保持存活
  • 问题:函数指针在重载后会失效

2.2 C 的解决方案:索引替代指针

c 复制代码
struct Task {
    int tag;           // 类型标签
    void* data;        // 指向具体任务数据
};

// 虚拟表(函数指针数组)
typedef void (*UpdateFn)(Task*, Environment*);

// 通过索引访问函数,而不是直接持有函数指针
table[task->tag].update(task, env);
  • 函数指针数组在每次插件加载时重新构建
  • 任务持有的是索引,索引永远不会失效
  • 索引指向的是重载后重新构建的函数指针

这本质上就是一个虚拟表

  • 可以指定在哪个 Arena 中分配
  • 掌控布局、查找方式
  • 完全暴露,无隐藏

2.3 为什么 C++ 的虚函数不能解决问题

尝试用 C++ 重新实现 Task 系统,使用标准的抽象类:

cpp 复制代码
class Task {
public:
    virtual bool update(Environment& env) = 0;  // 纯虚函数
};

结果:用 C++ 重写后,热重载时同样崩溃。

原因

  • C++ 编译器的虚表布局是**标准未定义**的
  • 无法访问/修改虚表指针
  • 虚表本身在 DLL 重载后会失效,且无法像手动维护的表那样重建

自定义虚表系统本质上比 C++ 的虚函数更适合这个场景,解决了 C++ 虚函数无法解决的问题


三、从 C 迁移到 C++

3.1 名称修饰(Name Mangling)

C++ 会对函数名进行修饰(mangle),导致符号名与 C 不兼容:

复制代码
C 函数:  hello
C++ 函数: _Z5hellov

方案extern "C"

cpp 复制代码
extern "C" {
    // 包裹不想被修饰的函数
    void say_hello();
}

3.2 编译错误与隐式转换

尝试将 C 代码编译为 C++ 时遇到的问题:

问题1void* 与具体指针类型的隐式转换

c 复制代码
// C 中:任意指针可自动转为 void*
// C++ 中:需要显式转换
char* ptr = (char*)arena_alloc(arena, size);

问题2decltype 语法

cpp 复制代码
// C++ 需要正确的 decltype 用法
auto type = decltype(arena.base)();

问题3:初始化顺序

cpp 复制代码
// C++ 构造函数的初始化列表顺序与变量声明顺序一致
// 而不是与初始化列表中的书写顺序一致
Wait(float duration) : duration(duration), cursor(0.0f), started(false) {}
// duration 必须在 cursor 之前声明

3.3 预处理器兼容性

编写兼容性宏,使代码同时兼容 C 和 C++:

c 复制代码
#ifdef __cplusplus
#define DECLTYPE(x) decltype(x)
#else
#define DECLTYPE(x) typeof(x)
#endif

四、现代 C++

4.1 模板与变参模板

cpp 复制代码
template<typename... Ts>
class Sequence : public Task {
private:
    std::vector<Task*> tasks;
    size_t current;
    
public:
    Sequence(Ts... args) : tasks({args...}), current(0) {}
    
    bool update(Environment& env) override {
        if (current >= tasks.size()) return true;
        bool done = tasks[current]->update(env);
        if (done) current++;
        return current >= tasks.size();
    }
};

示例

cpp 复制代码
new Sequence(
    MoveV2(position, vec2(200, 200), 0.5f),
    MoveV2(position, vec2(0, 0), 0.5f)
);

4.2 Placement New 与 Arena

当需要在 Arena 中构造 C++ 对象时:

cpp 复制代码
// 1. 先用 Arena 分配原始内存
void* memory = arena_alloc(arena, sizeof(MyClass));

// 2. 用 Placement New 在指定内存构造对象
MyClass* obj = new (memory) MyClass(args...);

作用

  • 不调用 malloc,让内存管理统一到 Arena
  • 显式告诉编译器对象生命周期的起始点
  • 支持编译器进行正确的优化

4.3 unique_ptr 与内存泄漏

尝试使用 unique_ptr 管理 Task 的生命周期,但遇到了模板参数推导问题:

cpp 复制代码
// 失败:模板无法推导
auto task = std::make_unique(MoveV2(pos, vec2(100, 100), 1.0f));

// 必须显式指定类型
auto task = std::make_unique<MoveV2>(pos, vec2(100, 100), 1.0f);

最终选择直接使用裸指针 + 手动删除,因为"不需要记住编译器想要什么"。

4.4 X 宏(X Macros)

一种 C 语言的编译时元编程技术,用于解决"多处需要同步更新"的问题

c 复制代码
// 定义列表
#define FRUITS(X) \
    X(apple)      \
    X(banana)     \
    X(tomato)

// 生成变量定义
#define DEFINE_VAR(name) int name;
FRUITS(DEFINE_VAR)

// 生成字符串数组
#define STRINGIFY(name) #name,
char* names[] = { FRUITS(STRINGIFY) };

// 结果:
// int apple, banana, tomato;
// char* names[] = {"apple", "banana", "tomato"};

为什么这里不适用

插件系统过于动态,有些任务类型在编译时并不知道,只有运行时加载 DLL 后才出现


五、 C 与 C++ 的讨论

5.1 "C 是 C++ 的子集"------这是个误解

一个经典反例:

c 复制代码
// 完全合法的 C 代码
void hello() {}
void hello() {}
cpp 复制代码
// C++ 中可以函数重载,所以允许
void hello() {}
void hello(char* name) {}  // OK

// 但在 C 中编译会报错:function redefinition

C 和 C++ 实际上是不同的语言

只是 C++ 设计时保持了语法兼容性,但这不意味着 C 代码在 C++ 编译器下能无缝工作。

5.2 C++ 的市场营销

Bjarne 大神是一位非常出色的营销者

C++ 成功的设计决策

  • 允许与 C 的二进制兼容(C 代码可以直接链接到 C++ 项目)
  • extern "C" 机制让 C++ 可以无缝调用 C 库
  • 增量采用:不需要重写整个项目,可以逐步引入 C++

这种设计让 C++ 成为"最容易与现有 C 代码集成"的语言,而 Rust 在这方面相对困难。


六、C++ 对 Panyim 有帮助吗?

没有

问题C++ 的虚函数机制无法解决 DLL 热重载带来的函数指针失效问题

C++ 的虚表机制没有暴露出来让我们自定义,所以无法解决我们的问题。自定义虚表和 C++ 的虚函数相比,在这个特定场景下是更好的选择

维度 C C++
热重载时的虚函数问题 自定义虚表 ✓ 完全掌控 虚表不可访问/修改 ×
增量采用 可以混用 C 和 C++ 可以
代码简洁度 需要 this-> 前缀 方法内可直接访问成员
内存管理 Arena 需要处理 unique_ptr
模板推导 N/A 常出现类型推导失败

附录

术语 解释
Name Mangling C++ 对函数名进行编码以支持重载的技术
extern "C" 阻止 C++ 修饰函数名,用于与 C 代码交互
Placement New 在预分配内存上构造对象,不调用 malloc
X Macros C 语言的编译时元编程技术,通过宏展开生成重复代码
Virtual Table (vtable) C++ 实现多态的内部机制,结构由编译器决定
Hot Reload 程序运行时动态重新加载代码,常用于插件系统
  1. 初始化顺序 :C++ 构造函数的初始化列表按成员声明顺序执行,而非书写顺序
  2. Virtual Distractor 问题 :基类有虚函数时需要虚析构函数,否则删除子类对象时不会调用基类析构函数
  3. C++ Modules:C++ 仍未有好用的模块系统
  4. struct 初始化 :C++20 开始支持 std::initializer_list =={} 构造临时 vector 再拷贝
相关推荐
nnsix2 小时前
设计模式 - 单例模式 笔记
笔记·单例模式·设计模式
雪度娃娃2 小时前
结构型设计模式——外观模式
c++·设计模式·外观模式
蜡笔小马2 小时前
05.C++设计模式-适配器模式
c++·设计模式·适配器模式
多加点辣也没关系2 小时前
设计模式-装饰者模式
设计模式
庞轩px17 小时前
第六篇:Spring用了哪些设计模式?——从单例到代理,拆解框架中的经典设计
java·spring·设计模式·bean·代理模式·aop·单例
多加点辣也没关系17 小时前
设计模式-工厂方法模式
设计模式·工厂方法模式
多加点辣也没关系21 小时前
设计模式-建造者模式
设计模式·建造者模式
多加点辣也没关系1 天前
设计模式-桥接模式
设计模式·桥接模式
雪度娃娃1 天前
结构型设计模式——装饰模式
设计模式·装饰器模式