AI

C Programmers Keep Committing These 6 Crimes

The C standard library is 50 years old and we're still writing it like it's 1974. I've reviewed enough systems code to know the specific crimes that kill readability — and most of them have a name, a pattern, and a fix that takes 30 seconds.

C Programmers Keep Committing These 6 Crimes

Someone merged a 4,000-line PR last week with no comments, variable names like tmp2, tmp3, tmp4, and a function called do_thing(). It passed review. It's in production. This is the water we swim in.

I'm not here to rant about C being old. C is beautiful when written well. The problem is the culture that's grown around it — the implicit badge of honor for writing code only you can read. Let's actually look at what this looks like in the wild.

Crime 1: The Void Pointer as God Object

This pattern shows up everywhere in embedded and systems code:

void process(void *data, int type) { if (type == 1) { struct Foo *f = (struct Foo *)data; } else if (type == 2) { struct Bar *b = (struct Bar *)data; } }

You've just reinvented dynamic dispatch, badly. No type safety. No documentation. The caller has to read the entire function body to know what they're passing. The Linux kernel uses tagged unions for a reason — look at how sk_buff handles this in net/core/skbuff.c. It's not elegant but at least it's auditable.

Crime 2: Macros That Eat Arguments

#define MAX(a, b) a > b ? a : b int x = MAX(i++, j++);

Classic. Both i and j get incremented twice depending on which branch executes. The fix — wrapping args in parens, wrapping the whole thing — is a 10-second job. The bug it creates can take a week to find. GCC's __typeof__ extension exists specifically for this. Use it or use an inline function. This isn't 1989.

Crime 3: Implicit Fallthrough as Load-Bearing Architecture

In 2019, a 200-person fintech I consulted for had a switch statement with 14 cases, 6 of which fell through intentionally, none of which had comments. The bug that cost them roughly $340k in a weekend of incident response traced back to a case someone added that fell through into a money-movement path. One missing break. GCC 7+ gives you -Wimplicit-fallthrough. It was off. Always turn it on.

Crime 4: Single-Letter Loop Variables Beyond Three Levels Deep

for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) for (int k = 0; k < p; k++) for (int l = 0; l < q; l++) // l looks like 1 in every font

l as a variable name should be a compiler warning. Seriously. Name it depth, channel, row, something. The 4th loop variable almost always maps to something real in the domain. Say what it is.

Crime 5: Error Handling via Return Code Convention Nobody Documented

Returns 0 on success? Or returns 0 on failure like strcmp? Returns -1 and sets errno? Returns a positive error code directly? All four conventions exist in the same codebase at some companies. I've seen it. The fix is one RETURNS comment at the top of every function. That's it. Three words and a value.

Crime 6: The God Header

One .h file, 6,000 lines, everything included everywhere. Compile times spiral. Circular dependencies emerge. Nobody knows what depends on what anymore. The include graph becomes a directed graph with cycles. At that point you're not writing C, you're doing archaeology. Forward declarations exist. Use them. Keep headers minimal. Your CI pipeline will thank you in wall-clock minutes.

What I'd Do Differently

If I were starting a greenfield C project today I'd wire up clang-tidy with a custom .clang-tidy config on day one, enforce -Wall -Wextra -Werror -Wimplicit-fallthrough from the first commit, and write a one-page conventions doc that answers exactly two questions: what does a zero return mean, and when do we use macros vs inline functions. That doc would've saved the fintech $340k. It takes an afternoon to write.

OPEN IN REEDL_ FEED →← Back to feed