In this post, we interview Nikita Lisitsa about C++, Rust, systems programming, and game development.
We discuss whether C++ is still the default path into systems programming, where Rust fits in, and how both languages may coexist over the next decade. Nikita also shares his perspective on memory safety, language complexity, game engine design, real-time physics simulations, renderer abstractions, and the lessons he learned from shipping Costa Verde.
.jpg)
Rust advertises its “if it compiles – it works” stance and its user-friendly/educational compiler messages. Should we start learning Rust as a first step to C++?
I’d say both languages should be learnt in parallel. Both Rust and C++ have a ton of quirks and idiosyncrasies specific to these languages (borrow checker in Rust, duck-typed templates in C++, etc), and I don’t think they are good prerequisites to each other. However, they share a lot, too (being low-level, RAII/Drop trait, caring about object ownership & lifetimes), which is why I think it’s a good idea to learn both at the same time.
Rust’s compiler and safety guarantees allow “learning by trial and error” in multi-threaded synchronization. Should we use Rust as training wheels while learning parallel programming?
I don’t have a strong opinion on this. Rust forces you to wrap shared data in Arc<Mutex<T>> or something like that, which does prevent data races, but also hides a lot of underlying complexity, and doesn’t prevent a lot of other problems like deadlocks. C++ has a lot of other stuff you have to care about when writing multi-threaded code. To be honest, something like Java still feels like the cleanest language to learn the basics of parallel programming, and then maybe C++ to learn the more complicated, low-level things. But for writing multi-threaded code, Rust is probably the best.
Generalising those two questions – is C++ still the best language to get into system programming?
I don’t think there is a “best” language for system programming, it really depends on what your goals are. Writing Linux drivers and writing high-load multi-threaded data-processing servers are very different tasks. C, C++, Rust, Go, and a ton of other languages can be great for system programming in a suitable context. C++ is generally a pretty good match, but sometimes the manual memory management and unexpected UB can make it not worth it.
C++ keeps accumulating features — modules, coroutines, reflection, contracts. Do you think the language is becoming too complex to use safely and teachably, or is that complexity justified?
The way I see it, there is a certain programming language spectrum between simplicity and complexity. It’s basically a balance of how much you have to keep in your head vs how much your language does for you, but you still have to keep in your head that the language does it for you. C, for example, is closer to the “simplicity” end of this spectrum, while C++ has always been on the opposite end. I personally love C++ for that: you have to learn a lot about what the language can do, but then it pays off as you use what you’ve learnt to your advantage to write simpler, more expressive code.
The current development of C++ follows that same pattern: more complexity that empowers the programmer. There have always been people who refuse to use most of C++ features, and only use some subset of the language, and it makes sense that from their perspective, the language is becoming worse. I personally think it is justified in the context of using C++ in its fullness.
Do you think C++ is a good choice for agentic development?
Never tried agentic development, so I can’t tell :)
With all this AI stuff going on, shouldn’t memory safety/correctness be the first priority of language design?
Memory correctness is just one type of bugs, and, contrary to the widespread misconception, it isn’t the most common bug we have in C++ or any other language. C++, for instance, has been evolving to make memory safety almost effortless — things like smart pointers and proper usage of RAII cover 95% of all such cases. The tooling has been evolving, too — ASAN, for instance, can find most memory-related bugs automatically. Typically, our bugs are simply errors in the program’s logic, and there’s little you can do in the language itself to prevent those. I’d say the focus should be on something like contract-based programming (or, better, proof-based languages that use dependent types / HoTT / etc), and not some specific type of common bugs like invalid memory access.
In game development/system programming, it is considered a good practice to avoid allocations at all costs. Did Rust people get it all wrong with all the fuzz around ownership?
The way I see it, allocations and ownership semantics are largely orthogonal. You can use stack variables, a dedicated frame allocator, or make an entirely custom global allocator both in C++ and in Rust.
That being said, I personally believe that the “avoid allocations” mantra is a bit of a cargo cult, and probably stems from the early 2000s, when OOP was the cool way to write code, and everyone allocated everything on the heap for no reason at all, leading to degraded performance. The right thing to do is always to analyze what’s the real performance drain. In my code, I often allocate tons of temporary arrays or strings (e.g. for UI), and it turns out that this is pretty much never the performance bottleneck unless you heap-allocate individual tiny objects or something like that.
C++ is still the language of choice for game engine development. What does Rust miss, and will it ever be able to compete with C++?
I don’t think Rust misses anything in particular. It can be hard to write complicated data processing code with nontrivial inter-object dependencies and a ton of mutable state (which is what game logic often looks like) in Rust, but it should be manageable once you do it a few times and settle on some fitting patterns and data structures. Also, game development often requires a ton of experimenting and ad-hoc tweaking, and Rust’s strictness introduces extra friction.
I believe the reason most game engines still use C++ is mostly inertia. I don’t just mean that people simply don’t want a new language when the old one does the job (though this is a reason as well), but also that C++ has several decades of surrounding tooling, libraries, and platform support (afaik various game console SDKs only support C++).
Your engine seems to provide low-level graphics/platform/audio/utility infrastructure, while you often reimplement the project-specific renderer per game. Where do you draw the boundary between “engine feature” and “game-specific system,” and can you give an example of a system you first put into the engine but later decided should live in the game?
I usually draw the boundary using two criteria: how good the code quality is, and how useful and generic this system is. Ultimately, it is based on how I feel about the code. There were instances of the code migrating into the engine and back. For example, there is a fairly generic deferred renderer in the engine, but I’ve never used it in any project, as I love playing with various styles and techniques and making the rendering engine from scratch is just easier. There is a 2d physics engine as well, which I never used outside of a few toy demonstrations — again, because it’s easier to make a dedicated simple physics engine tailored to a specific project.
On the other hand, the engine contains a very primitive 2D painter class that can draw lines, circles, sprites, and text. While the code quality and the API of this class are rather questionable, I’ve used it numerous times because it’s basically all you need for a simple 2D game on a game jam, which is why it’s part of the base engine as well.
In your soft-body spaceship work and water-over-terrain simulation, you repeatedly choose simplified models that preserve the gameplay-relevant behavior while avoiding full physical realism. How do you decide which invariants must be preserved, such as stability, mass conservation, energy behavior, or locality, and which physical inaccuracies are acceptable for a game?
It mostly boils down to how the physics simulation interacts with gameplay, and how costly the simulation is. In my soft-body game for Ludum Dare 53, you play as a soft squishy spaceship. Hare, stability is essential, as we don’t want the spaceship to explode, while conservation of momentum and angular momentum aren’t that important. In fact, I introduce artificial friction (which, of course, doesn’t exist in actual space) to dampen momentum over time, to help stabilize the ship if it’s rotating wildly, to prevent the player from accidentally flying too far away, and to force the player to think strategically about fuel consumption when planning distant trips. The simulation itself is extremely cheap, by the way, so performance isn’t an issue here.
Water simulation is way more complicated. Typical methods involve iteratively solving large sparse linear systems, with many iterations per frame. Together with my goal of simulating a reasonably large portion of terrain with a somewhat high resolution (say, 200x200 meters with 1 meter resolution), it’s not really feasible to do that in real-time. (I could do this on the GPU, but it’s already busy doing the rendering, and it only makes things faster by some constant factor.) So, I have to drastically simplify the model in order to achieve real-time performance. There are some properties of the model that I can’t sacrifice — for example, local mass conservation is crucial to prevent small puddles or lakes from disappearing (some models I’ve tried before had this problem). Energy conservation isn’t that important, though, — as with the squishy spaceship, I’ll introduce some friction anyway to make the water surface calm down over time. The method I’ve chosen (called virtual pipes) has a ton of drawbacks — for example, it lacks inertia entirely, — but it fits the other criteria, and is as fast as it could be.
You have used OpenGL 3.3, ported toward WebGPU, built WebGPU demos/raytracing, and used WebGPU compute for browser simulations. If you were designing your renderer backend abstraction today, what would you expose as stable engine-level concepts, and what would you avoid abstracting because WebGPU, OpenGL, and future APIs differ too much?
I actually don’t like the idea of a “renderer backend” for my personal projects. I usually rewrite the rendering engine from scratch for each project, using some rendering API like OpenGL or WebGPU directly. There are some things that seem common across many rendering engines, like textures or 3D models, but I find that it’s hard to make them truly generic because each project has its special needs. Maybe you need instancing, maybe you need some extra vertex or instance attributes to control per-vertex wind strength, maybe you need to pass an extra per-instance color transformation matrix, etc. If you try to accommodate all possible use cases, you end up with something that basically directly mimics the underlying API with no real benefit other than satisfying one’s need for abstraction. So, in some sense, I avoid abstracting the API altogether. This means that switching APIs for a particular project can be hard, but that doesn’t happen too often. In fact, it never happened before, but I might rewrite my current project in Vulkan. The rendering code is about 10k lines, and most of them aren’t related to the underlying API at all, so I don’t expect it to be that complicated.
Costa Verde was your first commercial release, and your projects show many jam games and technical prototypes. Looking back, which technical bets paid off when shipping Costa Verde, and which engine or design choices slowed you down the most? How would those lessons change your current village-building game’s architecture, production scope, and tool priorities?
This might sound surprising, but, for the most part, releasing Costa Verde didn’t really teach me anything about the technical side of game development that I didn’t already know. There weren’t really any technical bets either: I used well-supported technologies (OpenGL 3.3, SDL2, etc) which are known to work, and, well, they did work perfectly. The game isn’t really that rendering-intensive, so I didn’t need a sophisticated graphics API.
I did learn one architectural thing, though. The game features a lot of clearly-defined object classes, like road segments, cars, buildings, traffic lights (those were removed in the final game, though), etc, and they all were mostly just stored in big arrays. I had to support arbitrary removal from these arrays, so I had to use some generation-based handles to reference the objects instead of bare indices or pointers. If you look closely, this is basically half of the implementation of a typical ECS engine (the other half being the components — adding/removing/querying/etc). Realizing that I’ve made several independent half-baked ECS implementations throughout the game’s code convinced me to implement a proper full ECS engine for the next project.
The 3-year release cycle — is it the right cadence? Too fast, too slow, or does the cadence even matter given how long adoption takes?
I think it’s alright. The adoption of new standards is rather slow, but I’d guess if the standards were released faster, the adoption would be faster as well, and the same if it were slower. The programming language landscape is extremely diverse these days, and languages are forced to evolve as fast as they can. However, releasing too often leads to a versioning hell. So, to me the 3-year cycle feels about right.
How do you see C++ and Rust coexisting over the next decade? Competition, gradual replacement in some domains, or peaceful coexistence?
To me, it feels that it will be a peaceful competition, with varying rates of success of both languages in different fields. I don’t think Rust will be able to replace C++ entirely, but it definitely will succeed in some areas, especially where safety is a high concern. I’d say that Rust has a higher chance of replacing C, though there are other contenders for that as well (e.g. Zig or Odin).
The US government and NSA have recommended moving away from C/C++ toward memory-safe languages. Does that concern you, and how do you think the C++ community should respond?
I’m all for using Rust in safety-critical software, but I don’t think this recommendation makes sense for software in general. As I’ve said before, I believe that, in general everyday-use software, memory bugs are a minor problem, and by that logic we should all move to programming in proof assistants instead.
That being said, currently there are many proposals to make C++ itself safer, and while this hasn’t lead to any particular large language feature like a borrow checker, it’s still heading in an interesting direction.
Blog: https://lisyarus.github.io/blog/about.html
X: https://x.com/lisyarus
YouTube: https://www.youtube.com/@lisyarus
StackOverflow: https://stackoverflow.com/users/2315602/lisyarus

