Good style in C++ code

Of course programming is an art, and programs in C++ can be beautiful or ugly. And of course we all prefer beautiful code.

However, writing beautiful code is not just about aesthetics. Beautiful code is good for a whole load of practical reasons as well as artistic reasons. This web page introduces you to an appreciation of this art form, as well as explaining why it is important and giving you a starting point in becoming a great artist and critic.

1. Good style = readability

The most important point to learn about good style is that your computer program does not just communicate to the computer what you want it to do, but it also communicates to all human readers how you solved various problems along the way. In other words your program is a way of communicating to everyone (human or computer) your ideas.

In practice, e.g. at research level or in industrial/commercial environments you almost certainly will be working in a team. Then the advantages of readable code become obvious. In fact most organisations will have very specific guidelines on how code should be written. These are there because it has been found that, although following them may take 20% extra time (say) the total time saved per project is much much more than this, and in extreme cases the difference between readable and unreadable code can be success or failure of a project.

For small or medium sized projects you do on your own, it may well be that the only human reader of your code is yourself. Readability is still important. You will still have to debug, use and modify your code, in some cases weeks, months or years after you wrote it, and however clear you were about what you were doing at the time of writing, you almost certainly will forget some or all of the details when you come to review your work later.

Tip.

Always indent your code well so that it is obvious immediately which close bracket corresponds to which open bracket.

Tip.

Write your code as far as possible so that it is formed from a lot of small functions. Ideally every function should fit on a single computer screen. It is easier to read and understand a single short function than a long function. There is almost never any need for long complicated functions.

Tip.

Always comment your code. Comments need to explain any special features or ideas or tricks that you use, and any assumptions you make about the nature of the input(s). Comments do not have to explain anything that should be obvious to any good programmer from reading the code itself. I.e. comments should supplement the code, not repeat it.

Tip.

Always give a detailed comment on any function saying exactly what the function does and what assumptions are made about the input. A large number of bugs are due to misunderstandings in what a function does. In any case you can use a function much more quickly if you can read exactly what it does quickly from a comment. That way you are not relying on reading code that could be tricky.

Tip.

Always use carefully chosen variable and function names such as runningsum or errorcode or difference. Then it is obvious what they are intended to represent. Many mathematicians are lazy and use a , b , x and so on. These could mean anything!

You will find other conventions on variable names. It is common to indicate the type of a variable in a name, e.g. x_int and y_double. It is also common to have a convention for capital letters. Perhaps a name in all-capitals like PI is a constant whereas variables are all-lower case. However conventions vary. Certainly you should adopt a convention and stick to it.

2. Good style = reliability

If you want to write a few lines of code and there are a few different ways to do it, your choice should be based in part on which method of coding am I least likely to get wrong.

Tip.

Get into the habit of adding extra brackets in expressions if it adds readability and reliability. Don't rely on your knowledge of the operator table and rules of precedence.

Tip.

Avoid side effects in functions and expressions if possible. For example the code x = x + myfunction(x); may well cause a problem if myfunction is a function that uses non-const call-by-reference on its input. (What order is the expression evaluated in? You can't be sure.)

Tip.

C++ allows some very sneaky ways of using operators, especially assignment operators. Some randomly chosen and mild examples include

x = y + ( z = 1 + w ); // assign 1 + w to z and assign y plus the answer to x
if (x = y + x) {      // assign y+x to x and test if the resulting value is zero
...
}

The time to use these is when you cannot achieve what you want (efficiency, for example) without it, and the code is correctly commented as above. However both the above examples can be better written with a couple of extra lines, avoiding the sneakiness.

3. Good style = efficiency

Good code should be fast to compute. This is not that same as saying it should have fewer lines. Sometimes code with more variables is faster. Consider,

x = y*y + sqrt(b+2.0);
z = y*y + 2.0*sqrt(b+2.0);

It is clearer, and faster, to use new variables.

{
  double ysquared = y*y;
  double rootbplus2 = sqrt(b+2.0);
  x = ysquared + rootbplus2;
  z = ysquared + 2.0*rootbplus2;
}

Here I have made a scope for the new variables, but it is common to find that these values are useful elsewhere too, and reorganisation in this way makes for a much cleaner and faster piece of code.

Tip.

Use variables (with well chosen names) for intermediate values, to improve readability and efficiency.

Tip.

If you need to write efficient but difficult to understand code, wrap it in a function and make the purpose and action of that function crystal clear, so that other people can use your function directly, knowing exactly what it is supposed to do. Also, any changes to your function (if they are necessary) should not affect the rest of the program (unless the changes also affect the function's behaviour, but this is something you should be very wary of doing).

Tip.

All code that relies on special properties of the particular machine (operating system, hardware) you are using should go in a separate small function and documented as such. If you later switch machines you will know what you need to change.

4. Good style = extendability

One of the biggest problems that occur in many projects is to extend a program or suite of programs to do stuff that was beyond the original intention. One possibility is that you have a new problem and discover that an old project goes quite a long way towards solving it so want to reuse the work you did earlier. But the two projects are not quite the same so you must combine them somehow. Or you may have a new idea while working on one problem and want to incorporate it.

The jargon word for this is scalability. Some programming approaches scale well from small or medium sized projects to larger ones. Some do not. I will briefly mention a small number of the good and bad practices here, assuming you are writing a small-to-medium piece of code. You won't know about all the words here but will find them easily enough in any good book on C++ programming or on the web.

Tip.

Always code for a situation a little bit more general than you one have in front of you. A simple but trivial example (real world examples are more complicated than this, so you should imagine extra complications and extra work here in place of the triviality I present) would be when you want to have a function that computes the sum of its two inputs. So you could write

double sum(double x, double y) { return x + y; } 

Then later on you need the sum of three inputs,

double sum3(double x, double y, double z) { return x + y + z; } 

and four. But at this point we've run out of letters so write

double sum4(double x1, double x2, double x3, double x4) { return x1 + x2 + x3 + x4; } 

(Possibly at this stage bugs crept in because of inaccurate editing after copy-and-paste.) The next week, five inputs are required, and it finally dawns on you that you should have written

double sum(vector<double>& x) { 
  double runningsum = 0.0; 
  int n = x.size();
  for (int i=0; i<n; i++) runningsum += x[i];
  return runningsum; 
} 

in the first place. Not only will all the cases be able to use the new function, but we could have saved all that time wasted on cut-and-paste and editing and debugging. (And it is not easy going back and implementing the new sum because all the old sum,sum3,sum4 have to get changed in different ways.)

Tip.

If you code by copy-and-paste-and-edit your code is unlikely to be good. Any one of your (several independent) edits may have a problem. And if you need to change something (because of a bug you find elsewhere or a design error) you have to make the same change in several places, massively increasing the possibility of a mistake or another bug.

Tip.

Keep the number of "active" variables in any part of code to a minimum, and make sure all variables have the most limited scope possible. Then changing your code, adding a variable elsewhere is unlikely to have strange effects. Declare variables as const if possible.

Tip.

Avoid the use of "global" variables. All variables should live in some scope. Global variables are very difficult to maintain as a project expands.

Tip.

Use namespaces, structs, classes, and other more advanced C++ features to manage variables, especially if you are tempted by global variables.

Tip.

Use namespaces, structs, classes, and other more advanced C++ features to group a number of features of your problem together to help you design your algorithms.

Tip.

From the very start, design a general purpose system to report or log intermediate values or any other values you need to know about while debugging. Such a system can be a great benefit to a final program too.

Tip.

From the very start, design a general purpose system to report errors that may occur (these may be errors in user input, or errors in your code). The traditional advanced C++ feature to to do this is exceptions but it doesn't have to be this way, nor do exceptions have to be used for errors.

Tip.

Do not be lazy with checking for errors. Always check for errors and report them properly.

On many older versions of MS-Windows the more frequent problems was running out of memory. It became a common and easy and lazy route for many (or even most) programmers (who were charging for their work) to write code that, when an error was discovered, irrespective of what the error was, automatically put up a dialogue box saying "Error: insufficient RAM". I experienced this many times. Imagine the user whose hard disc has a problem who sees such a message: they might be tempted to go out and spends lots of money on more RAM. And they will be cross or at the very least totally confused when this does not solve the problem. I recall a software problem with a video driver which resulting in the message "Error: insufficient RAM" and an inscrutible error code. On consulting with the manufacturers I discovered that this error code meant something quite different and the problem was solved without buying more RAM.