The problem is not that the rules are hard to follow--they're actually quite easy! The problem is that, to follow the rules, you need to set up certain invariants, and in any appreciably large codebase, remembering all of the invariants is challenging. As an example, here's a recent memory safety vulnerability I accidentally created:
int map_value(map_t *map, void *key) {
// This returns a pointer to the internals of map, so it's invalidated
// any time map is changed. Therefore, don't change the map while this
// value is live.
int *value = add_to_map(map, key, default_value());
// ... 300 lines of code later...
// oops, need to recurse, this invalidated value...
int inner = map_value(value, f(key));
// ... 300 lines of code later...
// Hi, this is now a use-after-free!
*value = 5;
return value;
}
It's not that I'm too stupid to figure out how to avoid use-after-frees, it's that in the course of refactoring, I broke an invariant I forgot I needed. And the advantage of a language like Rust is that it bops me on the head when I do this.1) If you're gonna return pointers, don't use an allocation scheme that invalidates pointers in the implementation of map_t.
2) If you want to reallocate and move memory, don't return pointers, return abstracted handles/indices.
Both of those are completely possible and not particularly unergonomic to do in C. If you need to enforce invariants, then enforce them, C does have enough abstraction level for that.
I still follow KISS, and if you're writing 600+ line functions, it is little surprise you forget things in the tightly written logic of code.
When people focus on the _right_ implementation for _their_ problem they often find out that best practices like DRY or abstractions don't scale well at all.
Everyone starts with the best of intentions. malloc() and free() pairs. Then inevitable complexity comes in - the function gets split to multiple, then across modules, and maybe even across systems/services (for other shareable resources).
The mental overhead of ensuring the releases grows. It _is_ hard, and that's most definitely not a lie beyond any trivial implementation.
Surprisingly "just design systems that manage their memory correctly", as you said, is a very legitimate solution. It just so happens that those systems need good language support, to offload a good chunk of the complexity from the programmer's brain to the machine.
I've been playing this game long enough to see the fatal flaws built in, which grows complexity, staff, and the magnitude of the failures.
Renee Descartes, a brilliant philosopher and scientist always used to say that if he thinks A and the rest of the world thinks B, the wrong one is probably him. And he was one of the most brilliant humans we've ever produced!
Mind you, don't misread that as "you should act like a sheep" but in the opposite sense, that even if you're 100% certain others are wrong, you should still have the mental acumen to understand that probability isn't stacked in your favour.
People aren't burning their professional (or even hobby) time for years or decades working on tooling and languages that help in memory management just to help other's skill issues.
They find it an issue that needs better solutions, and that's it in C.
Andre Malraux was right: "The 21st century will be religious or it will not be".
He just got the definition of religion wrong.
Forgetting to free memory that is allocated and then used inside of a function is the rarest kind of memory management bug that I have run into in large code bases. It's frequently obvious if you read the function and are following good practices by making the code clean and easy to read / follow.
The ones that bite are typically a pointer embedded in some object in the middle of a complicated data structure that persists after a function returns. Reference counting may or may not be involved. It may be a cache of some sort to reduce CPU overhead from recomputing some expensive operation. It's rarely a chunk of memory that has just been allocated. To actually recover the lost memory in those cases is going to need something more complicated like garbage collection.
But garbage collection is really hard to retrofit into C when libraries are involved as who knows what kind of pointer manipulation madness exists inside other people's code.
What would be really interesting is if someone made a C compiler that replaced pointers with fat pointers that could be used to track references and ensure they are valid before dereferencing. Sure, it would be an ABI bump equivalent to implementing a new architecture along with plenty of fixups in legacy C code, but we've done that before. The security pendulum has swung over to the point that rebuilding the world would be considered worthwhile as compared to where we stood 10-15 years ago. It'd certainly be a lot of work to get that working compared to a simple hack per the Fine Article, but it would have real value.
https://github.com/bsenftner/kvs/blob/master/kvs/kvs.cpp#L72...
where it's then not clear why there is no call to
`kv.m_binarySize = byte_size`
after calling
`kv.mp_binaryData = (uint8_t)malloc( sizeof(uint8_t) byte_size );`
because it seems like `kv.mp_binaryData` will now have a different size than it had before. That is, there will be a mismatch. Though it should not affect the `free` call.
I hope I'm missing something because I just dealt with the code for around 5 minutes.