Robert's Rules (Of Coding)
- Improvement Techniques
- Debugging Techniques
- Coding Rules
- Commenting Rules
- Stylistic Rules
Improvement TechniquesNo matter how good you are as a programmer, you can always improve. I am always looking for ways to become a better programmer and these are some of the techniques I use to get there.
Get to know your compiler
For embedded programming it is important to know what code your compiler is actually generating. Create small sample programs and compile them. Look at the resulting code. You may be surprised!
For example, which of the following would you choose if you wanted the smallest, fastest executable?
index = ReadHardware(); if ( index != 0 ) DoStuff();or
if ( (index = ReadHardware()) != 0 ) DoStuff();With the compiler I am currently using, the first one actually generates smaller code. The second creates extra instructions to save the temporary value while it does the assignment.
Read the standard
When you come across something weird, get out your copy of the standard (K&R, ANSI, whatever), and read what it says about it. You may learn something new about the language, or what the designers really had in mind. Perhaps you will find an actual bug in your compiler. At any rate, you will probably wind up with a better understanding of how the language works, at least with your compiler. You will also learn to distinguish between coding for your compiler and coding for portability.
For example, look at the following code:
#if OPTION printf("Hello"); #else printf("Bonjour"); #else printf("Gut morgen"); #endifWhat will get printed for various values of OPTION?
I started examining this because of a typo in my code. After the compiler confused me by never compiling the last piece, I checked the standard and found that in a chain of #if, #else, and #elif, the first block for which the condition value is non-zero will be compiled and the following blocks will not. In other words, the standard allows the (non-obvious) #else #else construct, with the (also non-obvious) consequence that the second #else block will NEVER be compiled. The compiler I am using does not even generate a warning for this, so I have added it to the list of things that I check when I am getting a confusing bug.
When you are debugging, you will spend a lot of your time running the code and examining the problems that result. If you can't find the cause, even though you understand the problem, step back. Get away from the computer. Read your code carefully. Step through it in your mind. If you have analysis tools, use them.
I have a tool to list source after processing out the #if's. This has often helped me to find a missing or extra conditional compile. I recently created an AWK script to read comments from one of my programs and extract a state table. By comparing the extracted state table with the design, I was able to find some subtle editing errors, and an oversight in the design (which becomes obvious once the code has to implement it).
Use tools to examine your code. After all, a computer is much better at looking at thousands of lines of code than a person. If the tools don't exist, write them. They don't need to be marketable tools; they just need to help you get your job done.
For example, whenever I want a printout of some source file, I use a listing utility which removes the conditioned out code, generates descriptive headers for each function, and marks the nesting levels within each function. This allows me to concentrate on how the code works, rather than what the compiler needs to know about it.
Make small changes in the way you work. If you can make your way of coding a little bit better, and make that change into a habit, then after a while you'll be writing better code without even thinking about it.
When you are having trouble getting your code to work, or when you are trying to finish your project, or whenever you think it is necessary, try to explain your code. If your design is complete, and your code correctly implements the design, then you should be able to explain the design and how you implemented it without any hand-waving.
The design objective is to have a design which provides a solution to the problem, and which is "obvious" enough that you can explain it to another programmer who has, at most, limited familiarity with your project. If you have to skip past details, or if you find yourself getting confused, then you have probably found a weak spot in your design.
You should be able to explain what each variable represents, and how each part of the code contributes to the implementation. Watch out for variables which change their meaning as you explain different parts of the implementation; these tend to be a favourite place for bugs to hide. See also "Name things right".
Mental trick: When you are reading your code, try replacing the variable name with the description from the comment where it was defined. If the code still makes sense, then it has a good chance of working the way you designed it.
When you find a new way of debugging or a new way of avoiding some problem, write it down. The act of writing it down will help you remember it, and if you forget, there will still be somewhere you can look for all the helpful tricks you have found in the past.
If you are working as part of a team, I strongly recommend creating a "How To" document for each project you work on. This is simply a place to record the details of how to do all those things you wind up doing over and over again. Any time somebody asks a question about how to do something, that question becomes a candidate for the list.
Debugging TechniquesDebugging is basically the art of taking code which is basically operational and finding and repairing the few things which are wrong. If the code is not basically functional, then you should not be debugging it; you should be creating it. If there are a lot of things wrong, maybe it is time to look at a redesign.
When debugging, especially somebody else's code, remember that a lot of effort and a lot of thought went into creating the code. That means the code is probably the way it is for a reason. Debugging is not the time to be questioning implementation decisions, but to correct the implementation flaws. If decisions have to be questioned, then you may want to consider redesigning the module; but remember all that effort that went into the original design.
When you find a problem in your code, try to understand how the symptom relates to the code. If you find a problem, but cannot explain how it causes the symptoms you are seeing, you may wind up fixing the wrong problem.
I have often found that this has caused me to keep looking after I fixed what I thought was the problem. Several times, this extra searching has led me to find a deeper problem which was the real cause. Even when I don't find a deeper problem, looking for the explanation has usually led me to a deeper understanding of the code.
Recently I had a problem which was losing data. In my investigation, I found there was a problem in the code which would cause the symptom, except that the same problem existed in the previous version, and the previous version was working fine.
I initially found it hard to explain, but ultimately was able to explain why it happened in the new version and not the old version due to a timing difference introduced by another bug fix. From this, I gained a better understanding of that previous bug fix, and also was confident that this fix would really solve the problem.
After you find and fix a problem, search you code for other occurrences of the same problem. This will often save you from fixing the same problem again, and will sometimes lead you to a problem which may otherwise have been difficult to find.
Also, if you fix it now, while it's fresh in your mind, you will understand the fix, rather than needing to go through all the reasoning again later.
Recently I fixed a problem which was causing a program to lose data. After I found the problem, and understood what the root cause of the problem was (in this case, clearing a buffer without signalling another piece of the problem), I searched the code to find out where else the same situation occurred. This search uncovered a total of three places where the same potential data loss existed. When you go in to fix some code, you should make an effort to improve the code, not just to fix it. Try to write the modifications using the same style and commenting techniques as the existing code. If you create a new variable, give it the right name, not i.
Update the documentation for any code you modify. Don't leave undocumented code sitting there waiting to trap the next programmer. If you had trouble understanding some of the code, maybe the comments need to be clearer. Expand on what was written to explain it in a way that would have helped you.
If every time you fix a bug, the code gets a little better, rather than just a little bigger, then you can be confident that you will eventually run out of things to fix.
Coding RulesThe best way to avoid bugs in your programs is not to write them in the first place. In my experience, most implementation bugs (apart from typos) can be attributed to failings of human memory. It could be two copies of a function; one of which was updated, or using the wrong variable name, or changing one piece of code and forgetting to change another piece which is dependent on it.
Make variable and function names self-explanatory. They will affect your ability to write the code, and other people's (or even your own) ability to maintain the code later. Names will be taken as self-explanatory, whether you write them that way or not. I remember a function called "Putone" which sounds like a yucky colour. If it had been called "PutOne" I would have understood it immediately.
One trick for managing variable names is to provide a comment with the definition of the variable. This comment should be a concise explanation of what the variable represents.
int i; /* counter */gives you no clue about what the counter is counting, or how you can determine if it is doing the right thing, but
int numRecords; /* Number of active records in the file */immediately tells you what the variable is measuring, and anywhere you see it used in the code you can ask yourself "does numRecords contain the number of active records in the file?" You can even implement debugging code which actually checks to ensure that numRecords does what the definition says.
Recently, I was upgrading an automatic mail handler. I added a line to handle the "From " field at the start of every message by checking:
Unfortunately, the function Matches does not check whether the input line matches the supplied string, but actually checks whether it matches the supplied string followed by a blank. What makes this worse is that I had chosen the original name for the function.
Be careful when naming functions. Even if you are the one who will update the program, you may get confused by a name which tells you approximately what the function does. You will assume it does what it claims, and may introduce bugs by that assumption.
Make the function do what its name says it does. If it does less, the extra might be overlooked, and if it does more, that might give some unexpected side effects in a future version. Anybody reading the code is going to assume the function does what its name says, so make that assumption work for you, not against you.
You never want to access global variables directly from another module. In general, you can use a function (or set of functions) to access the variable. If you are programming an application where speed is EXTREMELY important (note: I have only seen this in embedded programming, and even then only rarely) then you can use an inline function, or fake an inline function by using a macro defined in the other module's header file.
The main idea here is to keep all knowledge and control of a module's variables with that module.
Commenting RulesComments are intended to help people who come back to read your code long after the program has been debugged. They are a communication tool to tell other programmers how the code works. The worst part about comments is that the compiler cannot write them for you!
Everyone has different ideas on how many comments are enough, and what comments should say. The best approach is to get in the habit of writing comments to describe the code. Once you are in the habit, the comments will flow more easily, and you will hardly notice the extra typing.
It sounds obvious, but it's a serious suggestion. Even if you're not consistent about it, and not very good at it, use comments! Whenever you can remember to do so, use comments to write notes about why you are writing the code the way you are, or what this variable is supposed to represent. The code will be easier to understand later if there is at least some clue about what you were thinking at the time.
The code already describes how the program works, and most of the people who read the code probably know how to read the language. Use comments to capture what the code can't. Use comments to explain the algorithm, explain your assumptions, note any ideas for future changes, etc, not to say the same thing as the code.
When you create a variable, you know what it is supposed to mean. Document that in a comment with the variable and you will be able to remember exactly what you intended when you look at the code many months later.
Once you get comfortable putting in comments, you can consider using pseudo-comments to make the algorithm extractable. If you mark your comments with a letter (for example, "/*P" for pseudo-code, or "/*V" for variables), you can write a simple script (for grep or awk, for example) which can extract the pieces you want. Extracting the pseudo-code (/*P comments) would give all the function names and the algorithms, whereas extracting the variables (/*V comments) would simply list the variables.
If you are using macros to emulate inline functions, document them to the same standards you use for functions. This will make it easier to understand them later, or to convert between macros and functions.
Stylistic RulesCoding style is another area where programmers traditionally disagree. Actually, I consider that a good thing. For one thing it shows that programmers are actually using a style (which means they can write yet another if statement without having to decide each time where to pu the brace. For another thing, as long as the debate rages on, good ideas will emerge from time to time, and the rest of us can slowly modify our styles by adopting the best ideas.
It seems that no two programmers can agree on style! The most important rule I've been able to come up with is "BE CONSISTENT". It is less important what style you use than how consistently you use that style. By sticking to one style you make your code easier to read because you eliminate the surprises that come with differing styles. You also give the reader some confidence that certain kinds of problems won't show up (for example, a style that insists all if's are followed by braces will probably not contain brace errors after if's).
Never put the body of an "if" statement on the next line without bracketing it in curly braces. If you do, I guarantee that someday you will add a line (debug code, perhaps, or a necessary later addition) without realizing that the line will not be inside the "if". The resulting bug is often quite difficult to find.
Use those braces
Document macros as if they were functions
Pseudo-comments make extractable documentation
Use comments to describe your variables
Comments describe, not transcribe, the code
Don't use global variables
Make functions do what they say
Name things right
Never write the same code twice
Leave the code better than you found it
Fix the whole problem
Understand the real problem
Record how you do things
Be able to explain your code
Change your habits slowly
Use tools to examine your code
Step back and look at your code