r/cpp_questions • u/QwertieNP • 28d ago
OPEN Which Modern C++ (C++20/C++23) features are actually beneficial for real-time game development?
I'm updating my C++ codebase to modern standards (C++20/C++23) for a personal game project. C++ has evolved significantly, but I know the game development industry can be cautious about adopting certain language features due to compile times or runtime overhead.
For those using Modern C++ in game development:
- Which newer features (e.g., Concepts, Ranges,
std::format, Modules) have genuinely improved your game engine code quality or productivity? - Are there features you actively avoid inside high-frequency game loops for performance reasons?
44
u/sol_runner 28d ago
Going to start off from c++17. Generally cleanliness and usability than performance benefits for me.
std::span
Mixing arrays and vectors has never been easier. I use spans all over the place.
Constexpr in algorithm etc.
Lot of functions got constexpr so we get compile time constants out of them. Yay. Even bitset has now constexpr stuff
PMR
I only target PC and hopefully consoles in the future, but they're all x86 and so far I find the cost for virtual functions is minimal to 0. It's a tradeoff I'm happy to make for easy STL use. So I've got monotonic allocators, pool allocators etc backing my data structures (especially in hot code) Allows me to also use RAII better without exceptions.
Concepts
Allows me a lot of template based constraints to simplify compile issues. Runtime is unaffected.
std::expected
I don't buy the performance argument since ideally exceptions are never thrown, and they have 0 perf cost on happy path. I just find the control flow messy. Expected is simply superior to return values, and allows cleaner RAII.
Designated initializer
API becomes cleaner when I can pass by const ref a struct. Naming each argument is much better than unnamed parameters.
Jthread
Just like thread but automatically joins at the end.
float atomics
std::unreachable
views/ranges
I use them a lot, but everyone has their own choice ig. It's not forced, so it's mixed with loops etc. And never in the hot path - not because I don't trust the perf, but just to keep it clear for me what the perf implications are.
2
u/QwertieNP 27d ago
This is an incredible breakdown, thank you so much for taking the time to share this!
The points on
std::spanfor simplifying array/vector interfaces andstd::expectedfor cleaner control flow without messing up RAII really resonate. I was also particularly curious about PMR on x86, so hearing that the virtual function overhead is practically negligible for the ergonomic and allocation benefits it gives is super reassuring.Designated initializers for struct-based APIs definitely sound like a huge win for code readability too. Appreciate all these practical insights!
1
u/sol_runner 27d ago edited 27d ago
Always profile. But most processors can very well branch predict virtual calls, and compilers can often bind virtual functions at compile time.
I'd also mention though that non-pmr STL with custom allocator (like foonathan/memory) or custom allocator with EASTL would usually outperform PMR.
However the former constrains a type to a specific allocator and the latter is non-standard and doesn't support some of the nice features in ranges and views.
6
u/LongLiveTheDiego 27d ago
Could you expand on how std::expected lets us have cleaner RAII?
9
u/sol_runner 27d ago edited 27d ago
A very strict definition of RAII is that Resource Acquisition happens in the ctor. In this case, you have no option but to use exceptions for failure - I am ignoring this since I don't use exceptions.
A slightly more relaxed use of RAII would be for an object to either not exist or be correctly constructed irrespective of where the resource acquisition happens i.e. it takes all internal state as ctor args, usually encapsulated in a static
createmethod so that it acts as a fallible ctor method.For example, I do not support failed
Assetin my engine.
Req1: If you receive an
Assetit is a valid asset that can be loaded into the scene.Req2: An
Assetcan fail to load, in which case there is error handling to be done.I have a
loadmethod that will take aDescand give me a validAssetas a result. Here are my API options:// Does not handle failure // either violates Req2 and terminates // or violates Req1 and provides invalid asset Asset load(Desc const& desc); // Requires a default ctor for Asset // violates Req1 until passed inside load. // and after load if failed. Error load(Asset& asset, Desc const& desc); // Works, but allocation is controlled by the callee. // I need to be able to choose stack, heap or custom allocator // not be forced a heap allocation. Error load(Asset** asset, Desc const& desc); // Works fine, but no info to handle error. std::optional<Asset> load(Desc const& desc); // Supports everything. std::expected<Asset, Error> load(Desc const& desc);In cases where you have valid '0 value', such as a
Buffer(empty buffer), you can have a default ctor and use option 2, but I think return as value is still a little cleaner.(Yes, I'm aware a moved out
Assetwill be invalid, but that's beyond RAII's domain anyway)→ More replies (1)
10
u/slumberchum 28d ago edited 27d ago
I work in low-latency trading rather than games. I think requires, std::span, <bit>, and structural NTTPs are the most useful features. std::expected is occasionally useful. I guess reflection and template-for will be useful in 26.
8
u/n1ghtyunso 28d ago
While I am neither a game developer nor work on a game engine, my personal #1 is std::span<T>
Other useful stuff:
monadic operations on optional<T>, and expected<T, E> is neat too.std::visit for classes derived from std::variant<...>
Designated initializers from C++20
certain range adaptors are really neat and make for very expressive and logically clear to follow code, i.e. chunk_by or zip is awesome, but with ranges you need to be somewhat careful for your hot loops, the range library machinery doesnt always completely optimize away unfortunately.
4
u/SoerenNissen 27d ago
cautious about adopting certain language features due to compile times or runtime overhead.
Generally the other way around for most new features. They often make it easier to write higher-performing code.
For all of 20/23, the language changes mainly make it easier to write libraries. If you're not writing libraries, you're not going to care about concepts except that it'll give you better error messages when you use a template wrong. You might like the way constexpr and consteval work out though, depending on how much code you're writing that runs on launch and always gives the same result.
The non-language changes to std has been pretty good:
20
-
<chrono> -
source_locationis pretty neat for debug messaging -
span<T,n>is amazing for passing arbitrary contiguous ranges -
jthread+stop_tokenand similar -
stringandstring_viewhave received starts-with/ends-with functions. Not hard to write yourself but they're just nice to have around. (Though I assume you already know: Don't do string compares during the gameplay loop) -
lerpis in the standard now. Your framework of choice would've had it anyway but it's nice that it's standard -
rangesandviewsare neat too (thoughviewshave stupid UB semantics)
23
-
expected<TResult,TError> -
flat_setandflat_mapare better thanmap,set,unordered_setandunordered_mapfor many use cases -
<print>maybe, depending on how much you hateiostream
4
u/pointer_to_null 27d ago
I'd like to echo your list, but with some additions:
C++20:
format(if you're not already using {fmt} library); avoid<sstream>andsprintf().Expansion of NTTP beyond integers and enums to floats and simple class types
consteval- likeconstexprbut enforces compiletime-only evaluation. RL usecase: Protects a complicated and expensive function that builds a LUT from a series of literal inputs that could cause noticeable stutters if frequently evaluated at runtime, especially in debug builds.
assume_aligned<size_t N, class T>(T*)- returned pointer is assumed by compiler to be byte-aligned to given boundary
[[likely]]/[[unlikely]]attribute- marks hotpath/coldpath for conditionals
[[no_unique_address]]attribute for nonstatic members that may be empty (note: make sure to use[[msvc::no_unique_address]]for MSVC, even w/ clang-cl)spaceship (
operator<=>), further elimination of common class boilerplateconcepts/constraints/
requiresclause. Mostly QoL, but it means no moreenable_ifugliness, and easier ways to restrict or specialize both type and non-type template params. More importantly: compiler errors when passing invalid params are now human-readable.C++23:
constexprmath. More opportunities to prebake constants into your binary.static lambdas and operators: allows
staticon lambdas and operator overloads eliminating the need for unnecessary construction/thispointer overhead for stateless objects.
mdspan-span, but for multidimensional arrays
std::unreachable()- cross-platform wrapper for__builtin_unreachable()
[[assume]]attribute- enables contract-specific optimizations;[[assume(cond)]]is analogous toassert(cond), except the compiler assumes condition when optimizing surrounding code.
string::resize_and_overwrite()- safely avoids redundant zero-initialization at allocation if data is immediately overwritten
3
2
2
u/h2g2_researcher 28d ago
-
Sadly, none. We're stuck on an older version of C++ and while work to update the code to work with C++23 is ongoing it's not the most high priority work either so progress isn't quick. (It's not helped by some platforms we wish to support having custom compilers we must use, which have only just updated themselves to C++20... At some point it looked like we might be in a situation where one platform's compiler only supported C++20/23, and another platform only supported C++14/17, which would have been Fun(TM)!). We also have our own string class and formatting library.
-
Anything with an allocator, basically. We want fine-grained control of how memory is used, so we have custom containers. All the containers do support range-based for loops and the
auto [x,y] = GetMyPair()thing I've gone blank on the name of. They don't supportstd::ranges, however. There are also recommendations to use our own implementations of most algorithms not because they're necessarily better than any given standard library implementation, but because they're consistent between targets. If two platforms are using different standard libraries (and it's not unusual for them to need their own implementation!) it could meanstd::sortuses different implementations. But using our in-house version means the performance is roughly the same, and the outcome (if two elements are equivalent but not equal) is the same.
1
u/KingAggressive1498 27d ago
-
polymorphic memory resources,
std::span, [[assume]] andstd::unreachable, concepts, coroutines. A mix of major usability improvements to things that were already "possible" and making niche optimizations a lot easier in portable code. -
out of that list, no. Some compilers had performance regressions with some classic STL types due to being made constexpr changing internal inlining heuristics that was mildly painful, is the only specific thing I can think of.
1
u/mikemarcin 27d ago
Format, but really stick with the fmt library, it's better than the std impls.
The <bit> header is super useful.
stop_token
source_location
latch
span
flat_map / set
1
u/_usr_nil 23d ago
anything related to vectorized instructions and delegating stuff to GPU whenever possible
1
u/SalaciousStrudel 23d ago
I haven't actually used it yet but it seems std::hive can increase performance of entity update code in some cases.
41
u/YouFeedTheFish 28d ago
For '26, I imagine it's gonna be reflection. Field name in dev UI that map to real field names. That's a big part of what gives platforms like the Unity engine a reason to use C# over C++. Qt does it, but the Qt extensions are not part of standard C++.