Clean C++ Code, and the Hidden Cost of Complexity with Sándor Dargó
On C++26, cognitive load, and the hidden price of clever code
Sándor Dargó has spent years making large C++ systems easier to maintain, safer to change, and cheaper to run. At Spotify he works on codebases where performance, binary size, and clarity have to coexist, and where the cost of getting any of those trade-offs wrong shows up in production. He writes daily about C++ on his blog, speaks at conferences, and sat down with Deep Engineering Live to talk about C++26, what it takes to write code that survives real-world conditions, and what the shift to AI-assisted development is doing to the way engineers work.
Watch the full conversation below.
A note on format: the transcript below has been lightly edited for clarity and readability. During the live session on Deep Engineering Live, questions were displayed on screen for Sandor to read, and we also brought audience members on stage to ask their questions directly.
Q. There is a thread running through your talks on clean code, binary size, undefined behavior, and now C++26. What problem are you actually trying to solve?
I try to reduce complexity in real-world systems. After all, I think that’s our main job as software engineers: to turn the complex into the simple. If you think about clean code, it clearly reduces the cognitive load. If you think about binary size, it might reduce operational cost, depending on your situation. It might even lead to more users, though I’m not sure I should delve into that one. Undefined behavior clearly reduces hidden risk. New standards like C++23 and C++26 reduce boilerplate and enable safe and more readable abstractions.
I think all of these topics connect. They make large C++ systems more maintainable and more evolvable. And most of my talks start from problems I actually encountered. I try to solve my own problems, but they are not unique. I just try to share what I learn on the go.
Q. From the vantage point of a staff engineer responsible for a large codebase, which two or three features in C++26 do you expect to most change everyday design decisions?
Everyone is talking about contracts and reflection. That’s going to change everything. I’m not sure about the time scale though. If you look at C++23 support right now, even that is not complete yet, especially if you look at the differences across compilers. You go on cppreference, check what’s implemented on which compiler, and we are simply not there yet.
Given that time scale, I’m not sure about the answer. But contracts and reflection are the big ones. I don’t think I’ll be able to use those in a production environment in the next one or two years. I hope I’ll be wrong.
Q. If you were reviewing an architectural proposal that leaned heavily on these features, what are the first red flag questions you would ask?
It depends on the environment. If we are in a widely-used production environment and these are very new features, I’d probably ask if those approaches are actually proven to work, and how maintainable they are. For the time being, we simply lack the experience with these new features. We are still trying to discover how to use them properly.
Being among the first adopters is sometimes good. Sometimes it’s better to be in the second line. It really depends on the environment. But I would look for already-proven, maintainable usage. This is pretty much what happens with almost all new major standard versions. There are people coming and saying, can we use modules? And you often end up saying, certainly not yet. There’s no cross-platform support. I can imagine that will be the case for reflection and contracts in the first few years.
Q. What does a responsible adoption plan look like for a big feature like contracts or reflection?
It really depends on your environment. If you target one platform and you know which compiler you’re using, and the feature was shipped as ready, then you can go ahead and try. But if you have to support different platforms or compile with different compilers in the same pipeline, you first have to check if all of them are supporting that feature properly.
In some of my earlier environments, I simply couldn’t start using even C++20 for a long time because not every feature we needed was supported on all the different compilers. In other teams, we said, okay, we use this compiler, it’s shipped, let’s go for it.
What you have to make sure is that even if for some reason you have to fall back to the previous compiler version, you don’t have to change your code. It would be quite a pity to move to a new version, start using concepts from C++20, and then in two weeks they say there’s a problem, we must go back. And then you realize it’s not just updating the compiler version, you actually have to change the code. So check that you have a safe fallback plan.
Q. Your talk “Clean Code, Horrible Performance” is a deliberately provocative title. What is the actual answer?
The title was a question, a provocative one. Someone very active in the community told me I shouldn’t have said it because it was misleading. Maybe it was. The whole point was to frame it as a question. My answer is no. Clean code does not imply horrible performance.
We must admit that in some constrained environments and on hot paths, you must optimize for performance and forget about readability. Otherwise your software just won’t meet its nonfunctional requirements. The most well-known example is probably how the square root function was optimized for Quake III. But in my experience, even in environments with very high throughput, readability and maintainability gained through clean code were always more important than optimized performance. Wherever I’ve worked, network latency and database read and write times dominated.
At the same time, I’ve seen people optimizing for heap allocations, saying we shouldn’t allocate for a string there, while at the same time they were making network requests in a loop. That just doesn’t make much sense. Amdahl’s Law says the overall performance improvement gained by optimizing a single part of a system is limited by the fraction of time that part is actually used. Slowness is also relative. If your code takes a long time to execute due to network latency, then relatively speaking, the heap allocation is not so slow anymore. I’m not saying you should put everything on the heap. I’m saying don’t worry about things that don’t really matter in your environment.
Q. If you had to write a one-page policy for a large C++ codebase covering the trade-offs between readability, performance, and binary size, what would be on that page?
If it’s a one-pager and you don’t know the exact environment or the nonfunctional requirements, it would probably be language-agnostic. The number one point would be: default to readability. You still read code more often than you write it. And not to mention agents, but they also prefer simplicity.
Second: if you have to optimize, measure first. Don’t start optimizing for performance before you prove it is actually a problem. You don’t optimize just because you can. You optimize if you need to. Otherwise, you might just waste time, or worse, you think you’re optimizing the necessary while you don’t touch the real problem. Measure first, optimize after.
Third: optimize only the hot path. You’ll find that with the measurements. Keep the hot path isolated and well documented. That will help you later.
And last but definitely not least: make the trade-offs very explicit. In code reviews, but also in the code itself. Leave comments. Because if you sacrifice clarity, document why. Otherwise someone later will come in and think, this doesn’t make any sense, let me make it cleaner. They are unaware of why certain choices were made. I’ve been there. I came in thinking something didn’t make sense, and by the time I realized it slightly changed the binary size in a way that mattered, some pull requests were already merged. Trade-offs there will be. But make them conscious and share the knowledge.
And this is probably even more important in the new world of agentic coding. Agents will not know the context that teams share with each other. You have to have things written down, and preferably in the codebase, because that’s what they can read.
Q. What are the silent killers of binary size that creep into C++ systems over months or years?
That’s a really broad topic. I wrote a series of articles on this and had a workshop at CppCon on the effects of programming styles on binary size. I made it very explicit that those articles and the workshop were not for embedded engineers, because they operate on a different scale and care about different orders of magnitude.
There are environments where every single byte matters. I never worked in such an environment. But there’s the other end of the spectrum where you might have to think about, bear with me, hundreds of megabytes. I know many of you might laugh, but I’m not kidding. When I first heard about binary size as a problem, it was due to a common library that many services shared. We hosted maybe two dozen services on that server. A few changed almost every week, others barely changed in a year. We had to keep ten or twelve different versions of the same library, and that library was over 100 megabytes. Just by making sure all services got the new version every few weeks, and we didn’t have to store more than two or three versions of that big library, we solved the binary size issue. That was seven or eight years ago.
In terms of programming patterns, the most overlooked area is unoptimized compiler and linker settings. You might gain the most from there. We tried many different code-level changes and it was satisfying, shaving off a few kilobytes here and there. Then we changed some settings and half a megabyte was gone. That’s often completely overlooked.
Template overuse is another one. We had a framework where adding a new object with no logic whatsoever, just the boilerplate, already added around 20 kilobytes because of the heavy templating. We moved away from that. Unnecessary use of std::function can also be problematic. We maintain our own backport of move-only function from C++23 specifically for that reason. And exceptions, while not always a silent killer, can be significant, though there’s interesting work being done to reduce their footprint considerably.
Q. How do you move code review conversations from taste to shared criteria?
Arguing over taste is never a good investment of time. Not just in engineering. You need to move from taste to agreements. And to do that, you have to communicate, discuss, and eventually decide. And it might not be very popular what I’m going to say, but your workplace is not a democracy. Certain people have more to say based on their experience and their responsibility.
But what’s important is that you introduce some shared decision framework. Track binary size in the CI pipeline so you see the effects at the end of every build. Track performance metrics. Once you have numbers, the conversation is not about taste anymore.
For the things you cannot quantify, like coding style, coding dojos are genuinely useful. You practice together, explore different approaches without delivery pressure, and over time you move from phrases like “I like this more” to “that’s actually the style we agreed on.” Discuss, educate together, share what you learn, and measure what matters to you.
Q. What are the most common mistakes when working with time and clocks in C++?
The most common mistake is choosing the wrong clock. Maybe you don’t fully understand the different guarantees each clock offers. For example, instead of using steady_clock to measure a short interval for a retry logic, you use system_clock. And then later, due to some bug, you figure out that system_clock is not a monotonic clock. It can jump backwards due to NTP adjustments or manual clock changes.
Another problem is unsafe conversions and cross-system time. Time is relatively easy when it’s in one system. But when you have different systems and different platforms, you can end up with clocks using different epochs, different precisions, or different time sources. When you try to compare or convert times from different systems, be very cautious. Test with all the different platforms. Debug and see what’s going on.
If you still use C-style APIs, things go wrong easily because they don’t give you the type safety that chrono durations give you. You might have to use C-style APIs, but try to isolate those parts and do the conversions at the boundaries. Within what’s within your control, rely on modern C++ time representations. Use chrono wherever you can.
For APIs specifically: keep your APIs abstract enough so that they are testable. Don’t rely on the system clock directly. Inject a time provider so you can test different assumptions about your code.
Q. You run a daily C++ quiz and have been blogging for years. What gaps have you noticed consistently, even in experienced C++ developers?
There are two main ones. The first is what I’d call a depth gap. C++ is a massive language. The standard is about 2,000 pages. Even if you are an expert in one area, it doesn’t mean you master all the others. You might be a master of template metaprogramming but know nothing about multithreaded programming. Best practices can be quite different across industries and across different kinds of C++ environments.
We should be humble enough to acknowledge our boundaries and say “I don’t know.” In the beginning of your career, it’s natural to do that. And with decades of experience, you’re confident enough to say it again. But in between, it’s more difficult. The sooner you can make that shift, the better. I once said in an interview that I didn’t know anything about a particular topic and didn’t want to guess. They said, well, we don’t really use that either, let’s skip it. I got hired at the end.
The second gap is fundamentals. I’ve seen many senior-level engineers who are really good at architectural questions, articulate and thoughtful. But they had difficulty writing some very simple algorithms under pressure. Not hard LeetCode problems. Simple ones. I’m not a fan of LeetCode-style interviews, but you do have to be able to solve problems live with someone watching. That’s something you won’t learn on the job. You have to practice on your own.
Q. What has the shift to AI-assisted development changed for senior and staff engineers?
There are two parallel shifts. One is the language itself evolving. With C++23 and C++26 we need a bit less template metaprogramming wizardry now that we have concepts. And safety has become a central topic in a way it wasn’t before. You can see that in the kinds of proposals the committee is now accepting.
The other shift is about how we work. As a developer, you’re expected to be professional in agentic coding. To be an AI-first developer, some would say. It’s as if you’ve become a team lead of agents. You keep giving tasks to them, reviewing the code, tuning your instructions.
Q. Viktor Nikolov joined us from the audience to ask a follow-up question on this. He wanted to hear more about the AI shift and what it means for engineers day to day.
I think as a developer in this new world, you have to learn to like your job again. Or still.
Before, you’d get your tasks at the beginning of a sprint or a week, and then you’d go back and start to explore the requirements, explore the code. It took some time. You slowly built up the models in your head and thought about the different kinds of solutions. You might even enter the so-called flow state, which requires focus and a bit longer time. And I think we’ve kind of lost this over the last few months.
We became, often, just prompters. Many of us complained even before that we are living in a world of constant context switching. But it just became even worse. Because at the same time, most probably, you will try to prompt different agents with different problems at the same time, and you keep jumping from one window to another. Maybe from one meeting to another, because others are also moving faster. At least they think they move faster.
Basically, you’ve lost everything, or almost everything, that you liked about your job. But we have to adapt somehow to this new situation. And mentally, it’s very difficult.
I read something very interesting recently on The Pragmatic Engineer, which is a great Substack if you haven’t come across it. They quoted research saying that in the beginning you ship more code, because it became so much easier. But you don’t just ship more code. You ship worse code. And that gain in speed is vanishing after a few months because you start accumulating technical debt at the same time. What first seemed faster becomes not faster, but the debt stays.
I also try different ways of working with agents that keep me happy but also try to speed me up, approaches that don’t remove what I like in this job but actually help. It’s difficult. And I’m happy to continue this conversation with anyone who wants to reach out.
Q. What would you tell engineers starting to build with C++ today, whether in a new codebase or an existing one?
The most common thing I see is defaulting to shared pointer when unique pointer is the right choice. People complain that smart pointers are slow, but they are defaulting to shared pointer instead of unique pointer, which is fast and cheap. Often you don’t really have to draw a line, you just have to know what to pick and not default to the easy option.
More broadly: performance is not for the sake of performance. You don’t write faster code because you can. You write faster code because you need to. If you don’t need it, default to readability and default to safety. And if you work in an environment where network latency or database latency dominates, you will not care so much about the cost of a heap allocation. Optimize for your actual environment, not your assumptions about it.
And document why you made the choices you made. Not what the code does, but why it is structured the way it is. That’s what makes a codebase survivable over time. Especially now that agents are reading it too.
Q. C++ versus Rust. Some engineers in the audience asked about this. Are C++ jobs being taken over by Rust?
I’m not sure how many jobs are actually being taken over. I had C++ colleagues who fell in love with Rust and moved to other companies just to use it. I don’t necessarily see that as a huge threat. C++ is not going away anytime soon, simply because it’s an old and evolving language and we have plenty of systems out there that you just won’t replace. Even if C++ is not strictly needed for a domain, the cost and risk of replacing it is too high. There will be COBOL jobs for decades for the same reason.
What I do think is that the overall pie of engineering jobs is growing. Rust is taking a bigger slice, but the pie itself is bigger. And moving between languages is becoming easier because agents can help you understand an unfamiliar codebase quickly. That lowers the switching cost over time.
C++ is already evolving. We are talking about C++32. The language is not standing still.
Sandor Dargo is a senior software engineer at Spotify, the author of a daily C++ quiz and blog at sandordargo.com, and a regular speaker at C++ conferences. This conversation was recorded live on Deep Engineering.


