Zero-Cost Abstraction Is a Myth: It Always Costs Readability
Zero-cost abstraction only ever meant zero runtime cost. Every interface, trait, and tiny function still charges you in readability. Here's the trade nobody admits to.
There's a phrase that gets thrown around in Rust and C++ circles like it ends the argument: zero-cost abstraction. You wrap your logic in a trait, an interface, a clever generic, and the compiler melts it down to the same machine code a hand-written loop would produce. No runtime penalty. Free safety, free reuse, all of it.
Here's the problem. The word "cost" in that phrase only ever meant one thing, and that was runtime cost.
And honestly, that's one of the most expensive blind spots developers have.
What "zero-cost" quietly leaves out
Ask a Rust dev what zero-cost abstraction means and they'll tell you something true: the compiler resolves the abstraction statically, so there's nothing extra to pay when the program runs. Iterator chains compile down to the same assembly as a raw loop. That part is real, no argument.
But runtime isn't the only place you pay.
You pay at compile time, because generic-heavy and template-heavy code is notorious for wrecking build times. You pay in complexity. And mostly you pay in readability, every single time someone who isn't you opens that file and tries to work out what it does.
That last one never shows up in a benchmark. So people act like it isn't a cost. It's the biggest one.
Abstraction is just a fancy word for indirection
Strip the jargon and here's what an abstraction actually is: indirection that can stand in for more than one concrete thing.
The word "animal" is an abstraction. It points at dog, cat, moose, whatever. It's useful because it's not specific. An interface is the same idea. A trait is the same idea. Even a plain function is the same idea. You take something concrete and direct, and you put a layer in front of it that could mean several different things.
Every layer is one more hop the reader makes before they hit code that actually does something.
So here's a claim I genuinely believe: most readability problems in any codebase come down to one thing, and it's too much indirection. Not bad naming. Not missing comments. Indirection. The reader can't follow a straight line through your code because you keep bouncing them somewhere else.
The trait-chasing problem
Mitchell Hashimoto, who co-founded HashiCorp and now spends his days writing Zig, put words to this. When he opens a big Rust codebase he didn't write, he hits something he calls trait chasing. He sees a trait method being called, and now he's spelunking through the whole codebase hunting for the actual implementation just to understand what one line does.
This isn't really a Rust thing, though. It's an interface thing.
Think about what an interface does to your reading. Before it, the code called a real function. You could click it, jump to it, read it, done. After it, that one call site has two, three, ten possible implementations, and you have to figure out which one is live before you understand anything.
That's a readability cost. You can't wave it away with "but it's statically dispatched, so it's free." Free for the CPU, sure. Not free for the human reading it at 11pm trying to ship a fix.
Functions are abstractions too. Yes, really.
This is the part most developers have never stopped to think about.
We all got handed the same commandment early on: break your code into small functions. One function, one job, keep them tiny. Clean code, right?
But a function is an abstraction. The second you pull a block of code out, give it a name, and call it from elsewhere, you've made indirection. That code used to read top to bottom in one place. Now the reader jumps to find it, reads it, jumps back, and rebuilds the flow in their head.
You traded reusability for readability. That's the trade, every time. Gain reuse, lose linearity.
A function is a smaller cost than an interface, because at least there's only one definition to go find. But it's still a cost, and pretending it's zero is how you end up in a codebase with forty-three two-line functions where you can't follow a single thought start to finish.
Carmack made this argument back in 2007
If "write long functions, inline your code" sounds like a fringe take, go read the email John Carmack sent his team in 2007. It still does the rounds, and his point was blunt: the popular advice to write many more functions and keep them as short as possible is often doing harm.
His reasoning is the same trade-off. Pulling code into a tiny function makes the caller read a bit cleaner, but it makes understanding the actual steps worse, because now you're bouncing around the file reading internals instead of seeing the whole operation in one place. He added something sharper too. When you're making a lot of state changes, having them happen inline forces you to stay aware of what you're actually doing. Hide them behind functions and the mutations slip past you.
People have been saying this for almost twenty years. We just kept teaching the opposite.
"But long functions are unreadable!"
This is where people push back, and they're picturing the wrong thing.
When someone hears "long function," they imagine one giant flat blob. No structure, no comments, a thousand variables leaking into each other. Yeah, that's bad. Nobody's defending that.
That's not the alternative. The alternative is a long function that reads linearly and is broken into block scopes, each with a short comment at the top saying what this chunk does. You get everything the small-functions crowd wanted, scoped locals that don't leak out, a label that does the job a function name was doing, without teleporting the reader across the file.
function processOrder(order: Order): Receipt {
// validate the incoming order
let validated: ValidatedOrder;
{
const total = order.items.reduce((sum, i) => sum + i.price, 0);
if (total <= 0) throw new Error("EmptyOrder");
validated = { items: order.items, total };
}
// apply discounts and tax
let priced: Priced;
{
const discount = discountFor(validated);
const tax = validated.total * TAX_RATE;
priced = { net: validated.total - discount + tax };
}
// build the receipt
return new Receipt(priced.net, order.customer);
}
Read it top to bottom and you know exactly what happens, in order, without opening another file. The const declarations inside each block can't leak into the next one. Each section says what it is. That's readable, and nothing got hidden.
The bell curve nobody wants to be on
Here's the uncomfortable shape of how a lot of us grow as programmers.
As beginners we wrote flat, simple, kind of dumb code. Long functions, direct calls, almost no abstraction. Mostly because we didn't know how to do anything fancier.
Then we hit the intermediate stage. We learned interfaces, generics, design patterns, dependency injection, and we got fooled into thinking this is what real programmers do. So we abstracted everything. Interface with one implementation? Sure, might need another one later. Factory for a thing that's built exactly once? Why not. This is where FizzBuzz Enterprise Edition comes from, that satirical repo solving a five-line problem with factories, visitors, and strategy patterns. Somebody even shipped a 2026 version with Kafka, LLMs, and distributed tracing. People keep rebuilding it because we all recognize that codebase.
And most developers stay there for their whole career. Stuck in the fat middle of the bell curve, sure that more layers means more skill.
The ones who climb out come round the other side and start writing flat, direct, dumb-looking code again. Same as when they started, except now it's a choice. They reach for abstraction rarely, only when the reuse clearly beats the readability hit, and they can tell you exactly what that trade was.
The part people don't say out loud
The developers who over-abstract the worst are often the ones who can't bring themselves to write simple code, because simple code feels like it makes them look junior. A long, plain, obvious function feels like an invitation to get judged. So they wrap it in three layers to prove they belong.
Writing genuinely simple code takes a bit of security. You have to be fine with someone glancing at it and thinking it looks too easy. The best engineers I've seen are completely fine with that, because easy-to-read was the whole point.
The one rule actually worth following
You don't need ten rules. One does it:
Cut indirection wherever you reasonably can, and add an abstraction only when you can name the reuse you're buying and you've accepted the readability you're spending for it.
Abstractions aren't the enemy. Interfaces, traits, functions, they all earn their keep when the payoff is real. The trouble is almost nobody, senior devs included, can articulate that there's a downside at all. And if you can't see the cost, you'll overspend it every time.
So next time someone calls an abstraction "zero-cost," ask them: zero cost to what? The CPU, maybe. The next person who opens this file is about to pay full price.
Write the dumb version first. Keep it flat, keep it linear, and add layers only when the code starts begging for them. Six months from now, lost in your own codebase, you'll be glad you did.
Written by Curious Adithya for Art of Code.