面向游戏开发者的 C++20 淘气与乖巧清单

C++20 的新特性在游戏开发者中引发了褒贬不一的反应:人们欢迎三路比较运算符、`std::bit_cast` 和协程等便利,但又担心编译时间、代码膨胀和工具链缺口。字符串格式化方面的 `{fmt}`/`std::format` 以及指定初始化器,体现了更安全、更具表现力的 API 与更重的模板或更严格的初始化规则之间的权衡,而这些对拥有 1000 万行以上代码的 AAA 代码库尤为重要。一些开发者正在向类似 Rust 的模式重构,或考虑彻底改用 Rust,认为在 C++ 的未定义行为和低层优化余地所带来的边际性能收益面前,安全性和易用性可能更值得。

三路比较运算符 <=>

  • 几条评论解释说,为某个类型定义 <=> 会自动生成全部六个比较运算符(<><=>===!=),从而减少样板代码。
  • 对于相似对象,这简化了实现,因为只需要“接通”一条例程。

std::format / {fmt}:性能、膨胀和编译时间

  • 有些人惊讶于看到 fmt/format.h 被批评;也有人同意,重模板和头文件-only 风格会拖慢大型 C++20 代码库的编译时间。
  • 一方认为,代码尺寸膨胀大多已被现代链接器,以及把重逻辑移到 .cc 文件所缓解;更大的问题是编译时间。
  • {fmt} 的支持者说它针对构建速度做了优化,性能可能优于 iostreams,并且在性能关键的数值格式化和 I/O 中被广泛使用。
  • 大家对为何建议的“分发到非模板 TU”模式不能同样用于 <format> 感到困惑。

大型游戏代码库中的 C++20 特性

  • 讨论集中在 1000 万行以上代码对于 AAA 引擎是现实的(例如 Unreal),以及增量构建,尤其是链接时间带来的痛苦。
  • 分布式构建和 Incredibuild 之类的工具有帮助,但链接器(尤其是 MSVC 的)仍然是瓶颈。
  • 有人质疑为什么有人会经常重编译超过几份文件;也有人指出,对核心头文件或编译器标志的改动会触发大规模重建。

指定初始化器以及 C 与 C++ 的语义差异

  • 许多人不喜欢 C++ 的指定初始化器必须按声明顺序书写,不像 C。
  • 理由包括:
    • 在 C++ 中,成员初始化和销毁顺序很重要。
    • 成员可以依赖前面成员的初始化(例如 int b = a + 1)。
    • 允许任意顺序会引入微妙的 bug,或者需要打破现有规则。
  • 有人认为可以放宽“类似 C 的” POD 结构体,但也有人指出,一旦结构体发生变化,这会很脆弱。
  • 几个人觉得 C++ 受限的指定初始化仍然有用,尽管不如 C99 的功能强大(没有嵌套链式写法,没有数组下标设计器)。

用于游戏引擎和动态加载的 Rust vs C++

  • 有位开发者因为人体工学原因想把 C++ 游戏引擎重写为 Rust,但担心影响进度,所以正在把 C++ 改造成“Rust-ready”(所有权、面向数据设计)。
  • 关于 Rust 和动态库存在争论:
    • 有人声称 Rust 一般“不允许”动态库;也有人指出 dylib 确实存在,但通常为了互操作性需要 C ABI。
    • 游戏工作流里像 DLL 热重载这种“普通 C++”能力被认为是巨大优势;实际上,C++ 和 Rust 在稳定插件边界上往往都要退回到 C ABI。
    • 也讨论了通过独立进程和 IPC 的替代插件模型,但被认为比进程内调用慢几个数量级。

有符号溢出 UB 与优化

  • 一位评论者认为,保持有符号溢出未定义所带来的性能收益很小(例如有时只是避免一条符号扩展指令),只影响特定的循环模式。
  • 其他人要求澄清;解释集中在编译器如何在“不会溢出”的假设下推理循环索引和数组下标。
  • 有人支持把有符号溢出定义为回绕(像 -fwrapv),或者保证溢出触发陷阱;现有的 UB 被看作是为极小的速度收益付出不划算的代价。

std::bit_cast、联合体与常量求值

  • 一些人很高兴有了 std::bit_cast 来替代基于联合体的类型双关;在 C++ 中这属于 UB,尽管编译器在实践中通常会“接受”它。
  • 这有助于审阅者推动更安全的写法,尤其是面对来自 C 的学生时。
  • 提到了 std::is_constant_evaluated(),但并没有真正展开;它对繁重数值/物理工作负载的好处在讨论串中仍不清楚。

协程与工具 / 可调试性

  • 对一些人来说,协程属于“乖巧”清单:它们可能比回调更快、更类型安全,而且通常能让异步控制流更易读。
  • 一个主要痛点是调试:协程崩溃的堆栈跟踪往往只显示框架/样板代码帧,而不是用户代码。
  • 也有人指出,调试大量回调的代码同样痛苦,但即便存在工具缺口,仍然更喜欢协程。

Ranges、lambda 与“非线性化”的控制流

  • 一种观点认为,大量使用 ranges 和内联 lambda 会让代码更难跟进:
    • lambda 外的代码会先执行,而 lambda 体可能稍后、重复多次,或者根本不执行。
    • 按引用/按值捕获如果没有仔细推理,可能导致微妙的生命周期和修改 bug。
  • 其他人承认这一点,但仍然觉得协程和更高层抽象比深度嵌套的回调更值得使用。