Writing Solid Code by Steve Maguire
Created on 2022-09-22T16:30:45-05:00
- Use compiler warnings and linters
- Explicitly declare assertions with assert statements
- Write integrity checkers that assert an object's current state or usage is sane
- Step through code with a debugger
- Functions should be sensible to use and test around
- Be adverse to using complex algorithms when a simple one does the job
Automatic error reporting: sending core/stack dumps to developers when an error occurs so users do not have to report them.
- Hungarian notation.
- Black Box testing essentially discovers bugs through luck.
- Making tools to automatically detect as many problems as possible.
- Screen code with dedicated linters before committing it.
- Use the unit tests. Avoid pushing bad code because you thought the change was so trivial you did not need to run them.
- Use preprocessors to keep debug and shippable builds of software.
- Enforce your assumptions; ex. if you depend on system-specific behavior then include tests that alert maintainers when building on a suspicious system.
- Assertions for "impossible" conditions can sometimes be useful.
- Using a "debug algorithm" which is slow but easy to verify the correctness of. The debug algorithm runs in parallel to the algorithm used when shipping the product and acts as a sanity test to detect when the complex algorithm diverges.
- Initializing memory to zero can hide initialization bugs; Microsoft still fills the buffers in debug builds but uses values like 0xCC so they are easier to spot and will crash if execution drops in to them. The exact fill value can be chosen dependent on the architecture (the book uses 0xA3 for Motorola systems since that results in a debug trap there.)
- Using fills when freeing memory in debug builds: dangling pointers have a greater chance to be detected because you see the canary bytes in them. This may require customizing your allocator to include the size of a block inside of the block so the free wrapper can check the block size.
- Try to run the shipping path along with the debug path if possible.
- Try to force "occasional" behavior to happen with regularity in a debug build; ex. force realloc to move memory to highlight dangling pointer issues.
- Book: Influence: How and Why People Agree to Things by Dr. Robert Cialdini
- Taking people to the more expensive item so the cheaper item seems more reasonably priced.
- Design interfaces to make it hard to use them incorrectly by accident.
- Avoid mixing error symbols with values. (Quinn: this is what Result types and multiple return values are for these days but those weren't around in C.)
- Avoid boolean arguments [what does emboggle("yis", true) mean?]
- Check for variable over/underflows.
- Strive to do a given task just the once.
- Try to contain special cases so later changes don't create problems.
- Rely on profiling instruments to make speed decisions. Sometimes making a procedure slower doesn't matter because its rarely used.
- Avoid touching memory you do not have ownership of.
- Beware of writing to output buffers. Document that it will be used as scratch space if it will be.
- Parasitic functions: when a function relies on private knowledge of another function such as how data is laid out when that layout is not public.
- Microsoft makes novice programmers do maintenance code and experienced programmers write new features. Knowledge of the code base is the path to ascent out of maintenence programming. This requires experienced programmers understand the people reviewing code later are less experienced and so archaic coding should be avoided unless necessary.
- Don't clean up code that is not important to a project.