Zero memory safety mistakes is a tall order. But the overwhelming majority of memory errors we see in the wild can be easily prevented by good practices. And for the memory errors that do happen stack protection flags make a big difference (https://developers.redhat.com/articles/2022/06/02/use-compil...).
At my day job, I work on a project that's 75% C++ and 25% C#. In the code that we've shipped, when there's a crash in code that I've written, (as opposed to business logic bugs) it's usually a memory safety mistake in C#. There was an interesting architectural choice written a decade before I joined the company where most classes have a synchronous constructor, then an asynchronous initializer, then an asynchronous uninitializer, then a destructor that gets run by the GC. There's no end to bugs relating to crashes because the initializer hasn't been run yet or the uninitializer has already been run.
When I get C++ bugs put across my desk in code that we've shipped to customers, it's usually a bug somewhere else. For instance, the last crash dump that we've gotten from customers in C++ is because a we called a Windows UTF-8/16 conversion function. The Windows function itself is all raw pointer nonsense, so we put a pretty wrapper around it so you put a std::string_view in and get a std::wstring out, or you put a std::wstring_view in and get a std::string out. Well, it turns out Windows is a fucking dogshit operating system. If you initialize a thread "wrong", ie, by calling the C11 function thrd_create, and your locale is set to a CJK language, it will ignore you when you tell it the code page of the multibyte string is UTF-8 and will assume it's the system locale's code page, and will call abort() when it hits a UTF-8 sequence. (instead of perhaps returning an error or null pointer) Those are the sorts of C++ crash bugs that I deal with.
My 'secret' to dealing with memory in C++ is to not deal with it. Make the STL do everything. Make RAII do everything. Can this object be POD with constexpr accessors? Do that. Can these methods be const? Do that. Can these objects live in a STL/boost container? Do that. Do they need to live in a unique_ptr/shared_ptr instead? Fine I guess but it would really be better off as a value instead. Does this class need to have a special destructor/copy constructor/move constructor? Find some other way to do it. If you must, try to find some other way to do it anyway. If you must, spend like 10x as much time scrutinizing it, the way a Rust programmer would do with unsafe. If thing must have special destructor/copy/move constructors, factor out all of the things that need special consideration into a class with the barest minimum. If it must have a special destructor, explicitly delete the copy/move constructors/operators if you can. Avoid indexing into arrays; use range based for loops, or <algorithm> stuffs like transform, reduce, or transform_reduce. The solution isn't to use vector::at() (which does bounds checks) instead of vector::operator[] (which doesn't) the solution is to not use indexes at all.
i'd recommend embracing hungarian naming (if you call it apps-hungarian you've not embraced it fully enough, read older documentation) and be as rigid as rust about using it. i-, c-, p-, and Max are your best friends, don't declare one of those without declaring all the others that you will need.
using hungarian, adopt naming conventions about Open/Close, Init/Finish, Alloc/Free, Start/End, Alpha/Omega, or whatever, and apply them rigidly to any class/struct (with indentation that's obvious). When you put in an Open, you go and put the Close in at that moment, just as you would close any paren you opened. don't return from all over the place in a function, goto endblock and free what needs freeing, you need to work on autopilot not thinking through the twists and turns to play monte carlo with getting it right. If you are going to return an allocation, your name needs to be Open, Init, etc. Use whatever words you want, but it's got to be something that makes you as the caller think "this is an open paren, i need to close it"
using ifdef debug type mechanisms, hook malloc and free and put guard word asserts at the beginning and end of every allocation, and magic number ids and reference counts in all structs, and hook main() and exit() so you check those for leaks. all. the. time.
work in a systematic way that avoids problems and make that your priority, everything else like functionality is slaved to that.
How do you "hook' a function, like you said about malloc,free, main and exit?
I guess it means intercept calls to it and do something additional to what it already does, something like Python decorators, but how do you do it in C or C++? I used C a lot (but not C++), but much earlier, and don't remember any method of hooking. atexit(), or something like it?
for normal/average operating environments, I prefer to just indirect through an extra procedure call, gives you a place to put a breakpoint, and then if you need streamlined performant code you can #define it all away. But, if you plan to #define it all away, make sure that is a regular part of your work flow beforehand. you can have a procedure version, a heavyweight define and a lightweight define (and use your defines inside your procedure to test them there). On a regular basis you need to make sure your infrastructure is all doing what you think it is.
Same is true for main(), use it for setting up infrastructure, have it call Main() which is "your main".
if you are on a large enough project you can budget time for tools, the actual "__main.asm" code that calls C main() is also accessible and you can hook away in there too. Have to do it for all compilers, and track compilers, but on the scale of a large project, that's not the end of the world.