Aussie AI
Defensive Programming
-
Book Excerpt from "Generative AI in C++"
-
by David Spuler, Ph.D.
Defensive Programming
Defensive programming is a mindset where you assume that everything will go wrong. The user input will be garbage. Anyone else's code will be broken. The operating system intrinsics will fail. And your poor helpless AI needs to keep chugging along.
Many of the high-level types of defensive coding are discussed elsewhere in this book. Good practices that attempt to prevent bugs include: assertions, self-testing code, unit tests, regression tests, check return codes, validate incoming parameter values, exception handling, error logging, debug tracing code, warning-free compilation, memory debugging tools, static analysis tools, document your code, and call your mother on Sunday.
Using Compiler Errors for Good, not Evil: One of the advanced types of defensive programming is to intentionally trigger compiler errors that prevent compilation. For example, you can enforce security coding policies:
#define tmpnam dont_use_tmpnam_please
Or if you are using debug wrappers for some low-level system functions, you can enforce that:
#define memset please_use_memset_wrapper
Politeness is always required. You don't want your colleagues going home crying.
Defensive Coding Style Policies: You might want to consider some specific bug-prevention coding styles, for defensive programming, maintainability, and general reliability. Some examples might be:
- All variables must be initialized when declared. Don't want to see this anymore:
int x;
- All
switch
statements need adefault
clause. - Null the pointer after every
delete
. You can define a macro to help. - Null the pointer after every
free
. If you use a debug wrapper for free, make it pass-by-reference and NULL the pointer's value insider the wrapper function. - Null the file pointer after
fclose
. Also can be nulled by a wrapper function. - Unreachable code should be marked as such with an assertion (a special type).
- Prefer
inline
functions to preprocessor macros. - Define numeric constants using
const
rather than#define
. - Validate
enum
variables are in range. Add a dummy EOL item at the end of an enum list, which can be used as an upper-bound to range-check anyenum
has a valid value. Define a self-test macro to range-check the value. - Use
[[nodiscard]]
attributes for functions. All of them. - Start different enums at different numbers (e.g. token numbers start at 10,000 and some other IDs start at 200,000), so that they can't get mixed up, even if they end up in
int
variables. And you'll need a bottom and top value to range-check their validity. You have to remove the commas from these numbers, though! - All allocated memory must be zeroed. This might be a policy for each coder,
or it could be auto-handled by intercepting the
new
operator andmalloc
/calloc
into debug wrappers, and only returning cleared memory. - Constructors should use
memset
to zero their own memory. This seems like bad coding style in a way, but how many times have you forgotten to initialize a data member in a constructor? - Zero means “not set” for every flag,
enum
, status code, etc. This is a policy supporting the “zero all memory” defensive idea.
Assume failures will happen: Plan ahead to make failures easier to detect and debug (supportability!), even when they happen in production code:
- Use extra messages in assertions, and make them brief but useful.
- If an assertion keeps failing in testing, or fails in production for users, change it to more detailed self-checking code that emits a more detailed error.
- Add unique code numbers to error messages to make identifying causes easier (supportability).
- Separately check different error occurrences. Don't use only one combined assertion:
assert(s && *s);
- Review assertions for cases where lazy code jockeys have used them to check return codes (e.g. file not found).
• Next: • Up: Table of Contents |
The new AI programming book by Aussie AI co-founders:
Get your copy from Amazon: Generative AI in C++ |