Zen of Defensive Programming: Compilers and Compatibility
Modern compilers are extremely powerful tools that have built-in optimization logic far outpacing the abilities of ordinary programmers. They are intimately familiar with the internal states of the processor at any given time and can optimize register usage, cache pipelines, core states, and performance bottlenecks better than most of us mere mortals.
Also, remember that your job is not to try and outsmart the compiler. Your job is to ensure you get the implementation right. The compiler’s job is to build the best possible executable code for it. The most tangible optimizations usually come from an optimization of the algorithm itself, not from trying to force certain code generation onto the compiler. The compiler is smart enough to do these things without your help. Incidentally, it will perform about a hundred other optimizations along with it that you are not even aware of. To get a taste of how modern compilers optimize code, take a look at this blog post by Matthieu Dubet.
Trust your compiler
There used to be a time when compiler optimization was frowned upon and anyone would tell you to disable it. In those days, it actually had the potential to introduce bugs. That time has long passed! Today’s compilers are so incredibly sophisticated and advanced that you should never opt out of compiler optimizations. Let it do its work as best as it can.
If you use common programming patterns for certain problems, the compiler knows exactly what you are trying to achieve. It can use optimized library functions or atomic implementations to get the desired result. If you obscure your intentions with some wild construct or arcane gimmick, you are robbing the compiler of this ability. It may try to optimize that code for you and you may end up with code that performs worse. I m not making that up!
Heed my warnings
Compiler warnings are one aspect of maintainability that is often underused. Many programmers tend to treat them as a nuisance. They will ignore them instead of seeing them as empowerment to craft clean and reliable code. Why would you want to miss out on such a great and free tool? Even interpreted languages such as Python allow you to adjust their warning level to give you a better overview of your code’s cleanliness.
To make sure your code is really clean and passes intense scrutiny, always set your compiler warning level to
extra. You may also set it to
pedantic but I found that setting to be excessively pedantic to be useful. Instead, I will often selective turn on certain warnings from the pedantic suite to get what I want.
If you have not worked on an elevated warning level before, you may find yourself staring at a wall of warnings at first. It can feel overwhelming. Be aware, though, that each warning could point out a potential bug that you have ignored up to this point. Furthermore, it will train you to be more aware of issues as you write new code. This will result in better code along the way.
Clean code has no warnings!
While most of these warnings may be simple type mismatches that you’d rather dismiss, keep in mind that in clean and defensive code, these type mismatches should not exist in the first place. If you find that you are writing a lot of type casts, take a step back. Check if, perhaps, you chose the wrong data type or implementation, to begin with. Well-designed code typically does not require a lot of casting.
The same is true for most other warnings. While they may seem harmless at times, always ask yourself, why was the warning triggered and how can you design your code to prevent it from happening. Then, make the new warning-free approach a habit. Remember, as Defensive Programmers, we try to keep an open mind and move towards paradigms that expose and avoid problems, not hide them.
If you haven’t done so, turn on your compiler warnings now. Then make sure your code always compiles without any warnings.
None at all!
There are no irrelevant warnings
If you think this is excessive, give it a try before judging. I am sure that while cleaning up all the warnings you are usually dismissing, there is a good chance you may find some real bugs in the process. I know, I have.
This brings me to the answer to the question I posed in my last installment. Why is it important not to suppress warnings for unused functions or unused variables?
The answer is that they might point to potential errors. Imagine you have two functions with very similar names and you accidentally used the wrong one, leaving the real one dangling unused. The warning could point you straight to the problem. We typically do not write code that has no purpose, so a warning that certain functions or variables are unused often indicates that we forgot something, even if it is only to clean up.
Don’t think of warnings as a nuisance you should suppress. Think of them as an automated tool to identify potential problem areas!
Compatibility is usually not high up on most programmers’ priority lists. The assumption is that it needs to run on a specific platform, so compatibility or portability is not a consideration. In the long run, however, this is almost always a fallacy.
Compatibility affects not only whether code works on a different platform. It also ensures code will still function correctly after a compiler update, for example. Whether your project still builds after you upgrade to the latest compiler version can be the ultimate litmus test for your code. Even more so if you just migrated to a different compiler suite. As a matter of habit, I will frequently switch between Clang and the Gnu compiler suite, to ensure it compiles cleanly on either. You would be surprised to learn how different the results can be.
To make this kind of compatibility possible, it is necessary to stick to generally supported idioms and language constructs. Naturally, you can decide which language-specific implementation you want to support (C++98 has very different features than C++23), but even within the language specification, there are often compiler-specific nuances. Learning them and avoiding them is key to making your code future-proof and keeping it compatible.
Oftentimes, however, the project itself will dictate the level of compatibility that is required. I’ve worked on projects where it made sense to re-implement functions of the standard libraries, just to ensure their compatibility. In another project, we did not use the standard library at all to minimize the memory footprint. Ultimately you will have to make a judgment call, but as with so many things in Defensive Programming, it is the awareness of the potential problems that is the really important part.
Debug your code
The most important thing there is to say about debugging is this: Every programmer should trace through every line of their code at least once!
To make it simple for you, if you have not started using a debugger, do it now! Do not wait, do not delay. This is a crucial step for any Defensive Programmer.
If you are intimidated by using debuggers, simply wean yourself into it. Start by loading in the program, setting a breakpoint, and going through your code step by step. Then, gradually begin using additional features, such as the inspection of variables, setting watches, conditional breakpoints, etc.
Most development environments provide visual debuggers these days for virtually any language. If you are using VSCode, you may simply have to grab a debugger extension for your language. One click and you’re ready to go, never having to leave your familiar environment at all. It could not be any more convenient.
Some platforms make it impossible to directly debug the hardware. Particularly in the embedded space where JTAG is not always available, it is often hard to debug your code. In cases like those, I will typically create a build for my desktop platform and then debug the code there. Naturally, this requires you to strip out hardware-specific code or put a hardware abstraction layer in place instead. Either way, I found this well worth the effort in the past. It allows me to trace through the core of my code with ease. More importantly, I don’t have to entirely rely on logging information, which is never a good substitute for a debugger.
Every programmer should trace through every line of their code at least once!
So, why is debugging so important? While you write your code, your mind is normally in a problem-solving mindset, trying to implement a solution. When you are debugging code, it forces you to switch to an entirely different mindset. One where the data and its flow is at the forefront. It makes you see the behavior of your implementation. You will observe information go through the system, and being processed. This is very different from the problem-solving mindset and often uncovers inherent shortcomings and oversights in your previously made assumptions. The number of glitches and errors you will find, and the number of things you overlooked or forgot will surprise you because seeing the gears grinding at work, gives you a whole new perspective!
Zen of Defensive Programming
Part I • Part II • Part III • Part IV • Part V • Part VI • Part VII • Part VIII
Thanks for stopping by. Preparing content such as the one you have just read takes time and effort to prepare. If you enjoyed it and you are using a Brave browser, please feel free to leave a small tip as a sign of your support by clicking on the small BAT icon at the top of your browser window. Your tip is much appreciated and it encourages me to continue providing more content such as this.