游戏引擎架构与现代C++工程实践笔记
(基于 Hazel Engine Code Review - Nathan Bags & The Cherno By Gemini)
这份笔记记录了“现代标准 C++ 规范 (RAII, Smart Pointers)”与“游戏引擎实战派 (Manual Memory, Intrusive Pointers)”在架构设计上的权衡与冲突,以及实用的工程经验。
1. 内存管理与生命周期 (Memory Management & Lifecycle)
• RAII vs. 显式生命周期控制
• 核心冲突:标准 C++ 倾向于 RAII(资源获取即初始化,析构即释放);游戏引擎倾向于确定性控制。
• 工程经验:
• 对于引擎子系统(如 Renderer, Physics),推荐使用显式的 Initialize() 和 Shutdown() 方法。
• 原因:析构函数的隐式调用顺序在复杂依赖下(例如:必须在销毁 GPU Context 前销毁 Texture)极难控制和调试。显式调用能确保“启动”和“关闭”顺序严格对称。
• 二段式构造 (Two-Phase Initialization)
• 原则:构造函数应轻量化,严禁在构造函数中执行可能失败的“重操作”(如分配显存、创建窗口句柄)。
• 实践:采用 Constructor (设置参数) + Init() (实际资源分配) 的模式。
• 移动语义 (Move Semantics) 的资源陷阱
• 风险:在实现移动赋值 (operator=) 时,直接用新资源覆盖 m_Data 指针。
• 修正:必须先释放 this 对象当前持有的资源(delete 或 Release),或者使用 std::swap 惯用法,否则会导致隐式内存泄漏。
2. 智能指针与资源所有权 (Smart Pointers & Ownership)
• 侵入式引用计数 (Intrusive RefCounting)
• 实践:引擎通常实现自定义 Ref 而非使用 std::shared_ptr。
• 优势:
1. 内存布局:对象与计数器在同一块内存,缓存友好。
2. this 安全:天然支持从 this 指针创建安全引用(无需 enable_shared_from_this)。
3. 多线程安全:配合渲染线程延迟释放资源。
• 性能优化:参数传递
• 原则:如果不涉及所有权转移(Ownership Transfer),严禁按值传递智能指针 (void Func(Ref ptr))。
• 修正:应使用 const Ref& 或原始指针,避免无谓的原子操作(Atomic Increment/Decrement)开销。
• std::function 与 std::unique_ptr 的冲突
• 问题:std::function 要求捕获的对象可复制 (Copyable),导致无法捕获 unique_ptr。
• 方案:
• (C++23) 使用 std::move_only_function。
• (Legacy) 被迫降级为 shared_ptr 或使用原始指针(需非常小心生命周期)。
3. API 设计与数据抽象 (API Design & Abstraction)
• 静态外观模式 (Static Facade)
• 模式:如 Renderer::Begin(),内部调用单例。
• 评价:虽破坏了纯面向对象设计,但极大简化了 Client API 的易用性,适合作为库的顶层入口。
• 内存块抽象:Buffer vs std::span
• 趋势:现代 C++ 应使用 std::span (C++20) 来表达“一段不拥有所有权的连续内存”。它标准化了指针+长度的组合,且支持 Range-based for 循环。
• C API 互操作陷阱
• 致命错误:将 std::string_view::data() 直接传给 C API (如 strlen, Windows API)。
• 原因:string_view 不保证以 Null (\0) 结尾。
• 修正:必须显式转换为 std::string 或手动确保边界。
4. 现代 C++ 陷阱与最佳实践 (Best Practices)
• Lambda 捕获 this
• 规范:[=] 隐式捕获 this 在 C++20 已被弃用。
• 风险:在异步/回调中,隐式捕获的 this 可能变成悬垂指针。
• 修正:显式写出 [this] 或 [self = shared_from_this()]。
• 类型转换 (Casting)
• 原则:严禁使用 C 风格强转 (Type*)ptr。
• 理由:类型不安全且无法搜索。
• 修正:必须使用 static_cast 或 reinterpret_cast。后者虽然危险,但它在代码审查时是“可搜索的 (Grep-able)”,能快速定位所有危险的内存操作。
• 枚举优化
• 技巧:使用 using enum MyEnum; (C++20) 来简化 switch case 代码,减少冗余前缀。
5. 跨平台与并发细节
• 线程亲和性 (Thread Affinity)
• 反模式:硬编码掩码(如 1 << 3)。应根据硬件并发数动态计算掩码。
• 宽字符转换
• 反模式:直接强转 (wchar_t*)char_ptr。
• 修正:必须调用平台 API (如 MultiByteToWideChar) 进行真正的字符集转换 (UTF-8 -> UTF-16)。
总结建议: 在业务逻辑层遵守标准 C++ 规范(安全优先);在底层引擎核心可采用侵入式设计和手动管理(性能与控制优先),但必须清楚底层的代价。