The C++20 Naughty and Nice List for Game Devs

C++20’s new features are prompting mixed reactions among game developers, who welcome conveniences like the three-way comparison operator, `std::bit_cast`, and coroutines, but worry about compile times, code bloat, and tooling gaps. String formatting with `{fmt}`/`std::format` and designated initializers illustrate the trade‑off between safer, more expressive APIs and heavier templates or stricter initialization rules, which matter a lot in 10M+ line AAA codebases. Some developers are refactoring toward Rust-like patterns or considering full Rust rewrites, arguing that the safety and ergonomics may outweigh the marginal performance wins tied to C++’s undefined behaviors and low-level optimization leeway.

Three-way comparison operator <=>

  • Several comments explain that defining <=> for a type automatically generates all six comparison operators (<, >, <=, >=, ==, !=), cutting boilerplate.
  • It simplifies implementation for similar objects since only one routine needs to be “plumbed.”

std::format / {fmt}: performance, bloat, and compile times

  • Some are surprised to see fmt/format.h criticized; others agree that heavy templates and header-only style hurt compile times in large C++20 codebases.
  • One side argues code-size bloat is mostly mitigated by modern linkers and by moving heavy logic to .cc files; the bigger concern is compile-time.
  • Defenders of {fmt} say it is optimized for build speed, can outperform iostreams, and is widely used in performance-critical numeric formatting and I/O.
  • There is confusion about why the suggested “dispatch to non-template TU” pattern couldn’t also be used with <format>.

C++20 features in large game codebases

  • Discussion centers on 10M+ LOC being realistic for AAA engines (with examples like Unreal), and the pain of incremental builds and especially link times.
  • Distributed builds and tools like Incredibuild help, but linkers (notably MSVC’s) remain a bottleneck.
  • Some question why anyone often recompiles more than a few files; others note that changes to core headers or compiler flags trigger huge rebuilds.

Designated initializers and C vs C++ semantics

  • Many dislike that C++ designated initializers must follow declaration order, unlike C.
  • Justifications include:
    • Member initialization and destruction order matter in C++.
    • Members can depend on earlier members’ initialization (e.g., int b = a + 1).
    • Allowing arbitrary order would create subtle bugs or require breaking existing rules.
  • Some argue that “C-like” POD structs could be relaxed, but others note this would be fragile when structs change.
  • Several find C++’s restricted designated init still useful, though less powerful than C99’s (no nested chains, no array index designators).

Rust vs C++ for game engines and dynamic loading

  • One developer is tempted to rewrite a C++ game engine in Rust due to ergonomics, but fears schedule impact, so is making C++ “Rust-ready” (ownership, data-oriented design).
  • There is debate over Rust and dynamic libraries:
    • Some claim Rust “doesn’t allow” dynamic libraries in general; others point out dylib exists but often requires C ABIs for interoperability.
    • Game workflows like DLL hot-reload with “normal C++” are cited as a big advantage; in practice, both C++ and Rust often fall back to C ABIs for stable plugin boundaries.
    • An alternative plugin model via separate processes and IPC is discussed, but seen as orders of magnitude slower than in-process calls.

Signed overflow UB and optimization

  • One commenter argues that the performance benefit of keeping signed overflow undefined is small (e.g., sometimes avoiding a sign-extension instruction), affecting only specific loop patterns.
  • Others ask for clarification; explanations focus on how compilers reason about loop indices and array indexing under the no-overflow assumption.
  • There is support for either defining signed overflow to wrap (like -fwrapv) or guaranteeing overflow traps; current UB is viewed as a poor trade-off for a tiny speed gain.

std::bit_cast, unions, and constant evaluation

  • Some are relieved to have std::bit_cast to replace union-based type punning, which is UB in C++ even though compilers typically “accept” it in practice.
  • This helps reviewers push safer patterns, especially for students coming from C.
  • std::is_constant_evaluated() is mentioned but not really explored; its benefits for heavy numeric/physics workloads remain unclear in the thread.

Coroutines and tooling / debuggability

  • Coroutines are on the “nice” list for some: they can be faster and more type-safe than callbacks and often make async control flow easier to read.
  • A major pain point is debugging: stack traces for coroutine crashes often show only framework/boilerplate frames, not user code.
  • Some note that debugging callback-heavy code is also painful, and still prefer coroutines despite tooling gaps.

Ranges, lambdas, and “non-linearizing” control flow

  • One perspective is that heavy use of ranges and inline lambdas makes code harder to follow:
    • Code outside lambdas runs first, while lambda bodies may run later, multiple times, or never.
    • Captures by reference/value can lead to subtle lifetime and mutation bugs if not carefully reasoned about.
  • Others acknowledge this but still find coroutines and higher-level abstractions worth it compared to deeply nested callbacks.