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.hcriticized; 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
.ccfiles; the bigger concern is compile-time. - Defenders of
{fmt}say it is optimized for build speed, can outperformiostreams, 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
dylibexists 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.
- Some claim Rust “doesn’t allow” dynamic libraries in general; others point out
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_castto 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.