Practice of writing good code
Tomasz Gil © 2002
What and Why?
This is a quick overview of programming precepts that I find most important
to observe by both novice and seasoned programmers so that they may generate
readable and maintainable code. The question of good software design is much bigger
and outside the scope of this overview of merely programming habits.
It is my opinion that communicating a few good ideas in a concise form goes a long way.
Style
- Block Scope and Indentation
- each pair of braces defines a code block, a block determines the
scope of locally constructed objects and variables
- indent code corresponding to a pair of braces (incl. the for, while,
if, elseif , etc - constructs) 4 spaces or 1 tab stop
- declare all variables and objects at the beginning of the block
- Variable names
- use self-explanatory pseudo-English-Computerese to form variable
and method names to be used in class declarations - do not exceed 30 chars,
typically 10 chars
- use short, conventional names for local variables - e.g. i for integer
index, x for x
- Parentheses are used to force precedence of operations in expressions.
Rather than rely on your imperfect recollection of precedence rules
enforce your own precedence of evaluation and deliver code clarity. This
applies most often to complex logical expressions.
Process
- File Layout
- use macros in header files to guard against multiple inclusion
- designate one header file to be mandatorily included as the
first include in all compilation units, this helps in configuration of basic
definitions for the whole project
- Compilation dependencies. Each file with C++ source code is compiled
as a separate compilation unit. This code knows about code defined in other
compilation units through inclusion of header files with code declarations.
Each C++ code compilation unit should include only the headers that are necessary
for compilation. Furthermore, a header file may need to include other headers.
An often encountered problem is when the necessary headers also contain other
declarations unnecessary for the given compilation and include other header
files for the sake of those other declarations. This leads to growth of spurious
dependencies. Things the programmer should do to prevent this:
- include only the necessary headers in the C++ source (.cpp) files
- try to replace the headers included in other headers with forward
declarations (e.g. class typeA; instead of #include <typeA.h>)
- do not place unrelated content in the same header files - in practice
this usually means separate header files for separate classes
- do not pile up code on a single line of text. Break up longer expressions
into separate lines. You will have an easier time debugging.
- Linkage dependencies. While writing code always keep in mind what services
provided by external libraries the code requires. The linkage dependencies
should be consistent with the project's design.
Substance
- const qualifier: "const typeA *p"
means that object of typeA accessed by pointer p may not change its state
and will accept only invocations of methods qualified as const. Examine individual
methods to see if they can use:
- the const qualifier in arguments - the method will not change
the passed in objects
- return values - the caller cannot change the content of the return
value
- method signature - the method will not change internal state of the
object
It is good to expose the fact that a method has certain conservative characteristics such as const.
- A thread of control passing through code by a sequence of calls and returns is
the standard programming paradigm. Upon method call a new stack frame is added
to the thread's stack, while upon return a stack frame is removed.
Especially when you have many lines of code in a method try to organize the flow control
in it so that you only need one return statement. This practice usually forces the programmer
to more carefully design the methods and facilitates code instrumentation.
You should keep in mind that objects passed in and returned
from methods are copied between stack frames by their copy-constructors.
Pointers and references are simply copied as they are indirections into objects.
- Exceptions. In addition to simple call/return code we use exceptions which are
essentially long returns. A method throwing an exception constructs an object and "returns" it
into a catch handler that can be many stack frames away. Catch handlers are associated with
guarded blocks of statements designated by the "try" keyword. As an application
of the "one-return statement" rule you dont not use "return" inside a "try" or "catch".
When the exception flies from "throw" to "catch", multiple stack frames are released
and destructors for objects local in those frames are invoked.
- static methods - if a method has no use for the "this" object instance
it should be declared static. A class static method will still have access
to private fields in any instance of the class.
- there should be rarely methods that return objects they construct with
new or delete objects passed in arguments. Such methods are factories and
may exist only as a part of a careful design. There is very rarely a need
for an object to delete itself - a method executing "delete this".
- use local variables inside methods to achieve clarity of the logic -
compilers nowadays do an excellent job optimizing such code.
- references are the same thing as pointers, except that you dereference
them with . and not with ->, and they cannot be NULL
- casting object pointers - avoid as much as possible, if you use it
you must be sure the cast is valid. If you have to cast a const pointer to
non-const pointer it usually means you want to perform a non-const operation
on the object and require a non-const method. The necessity of casting
typically indicates bad software design.
- casting simple types, e.g. double to int, may hide loss of value -
you must be sure this is not happening or that you can silently tolerate it
- constants - have them named - defined in one place and used by name
in many places - then they can be changed by changing code in one place.
The way to define them can be #define or enum - but enum should be preferred
and used within class declarations
- expressions evaluate to a value of a definite type. You should be
aware of the type while writing an expression.
The value of an expression is not stored in any definite
place but it can be:
- assigned to a variable - a copy of expression type is created
- used as argument of the enclosing expressions
- passed as an argument of a method - a copy of expression type
is created
- returned as a return value of a method
- the assignment operation is also an expression - the value of this
expression is the resulting left-hand value. Please do NOT use this legacy
feature of C/C++ syntax. The common temptation is to use assignment as argument
of another expression. This creates confusing code.
- use appropriate type of expression (logical) in control constructs
such as while, for, if. Do not rely on the compiler to interpret NULL pointer
as false - as is the case with the C compiler.