I am yet to see someone who can't make memory safety mistakes in C++. You might want to start a tutorial series on how to do that. If this skill can be taught that is, and isn't genetic.
Memory safety issues in C++ have two main causes. One is object lifetime. The other is pointers. If at all possible you want to design your program such that object lifetime is completely obvious and predictable. Use sentinels and tombstones instead of null pointers. Use arenas wherever possible. During debug use an allocator that doesn't reuse memory (just mark the pages as no access so you will segfault when trying to read/write in previously freed memory). When object lifetime is complex you can give objects a "color" during allocation and then make rules that you can verify that objects of one color can never have pointers to objects of another color, or that an object can never contain a pointer to a younger object. You can eliminate entire categories of memory problems this way. Instead of pointers use indices and a getter function. In debug mode the getter can check if the right locks are held, scan the heap for incorrect pointers, check for ownership flags, etc. Actually take advantage of the virtual memory tools provided to you by the operating system. Threadlocal memory. Fork tasks into different processes. Actually use mprotect and the like.
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...).
I don't claim to never make memory safety mistakes in C++. I just claim that it's very rare. The overwhelming majority of my bugs are bugs in business logic.
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?
Given that they mention ifdef, I guess they just mean something like `#define malloc(x) (tracing_malloc((x))) ` in every place except the one where the tracing version is defined.
hungarian standardizes procedure/function names anyway (typed and capitalized), so while you're changing the name, it gives you "a place to stand" to do the other things you want to do.
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.
I should take this to the security team although I have a hunch that they'll think I am full of shit. What with the CVE count never going down with asan, ubsan, tsan and all kinds of guidelines, analyzers, tests and what not.
Hungarian notation has never been proven to reduce errors compared to strong types, and the CppCoreGuidelines tell you not to use it. clang-tidy can largely automate Hungarian notation, at least.
the goal of hungarian is to ease/automate the cognitive burden on the programmer while in the act of reading or writing code, it's the semantic notion, not the syntactic