Open Graph Drawing Framework
current version:
v.2012.05 (Madrona)
     

Programming conventions

Assertions

Finding bugs in code is a tedious but inevitable task in the software development process. Hence, it is very useful to put various tests into the code that check that functions are called correctly, that data structures remain consistent, and that invariants hold. Such tests automatically check for errors and guide the programmer to the real sources of errors. However, performing these tests is time-consumable and should be avoided in production code.

We maintain both debug and release code using conditional compilation. The debug version is indicated by the compiler define OGDF_DEBUG. Critical conditions can be certified using the assertion macro OGDF_ASSERT, e.g.,

template<class E>
E &Array<E>::operator[](int i)
{
    OGDF_ASSERT(m_low <= i && i <= m_high)
    return m_vpStart[i];
}

The condition is only checked during debugging. In a release build, the assertion macro expands to empty code and thus does not produce any performance loss. The following recommendations have to be observed:

  • Use assertions to certify that preconditions for function parameters hold. If you find you cannot validate a particular argument because you have not enough information, consider to maintain extra debug-information.
  • Use assertions if necessary to check that conditions or invariants hold inside a function.
  • Use assertions to detect impossible conditions. If such an impossible condition occurs, either this assumption is false, or there is a bug somewhere else in the code.
  • Do not use code inside an assertion statement that has any side-effect. Be aware that this code is not executed in the release version.
  • Do not check error conditions of functions with assertions, e.g.,
    int *pTable = new int[tableSize];
        OREAS_ASSERT(pTable != 0)

    is a programming error. According to the specification of the new operator, the return value may be 0 indicating that there is not enough memory available. If a function may return an error value, this value has to be checked and the calling function must handle the situation. In particular, not checking the return value of new or malloc() is a serious programming error!

  • If it is not clear or obvious what your assertions verify, make sure to include comments to explain the tests. Unfortunately, when programmers get an assertion failure and do not understand the purpose of the test, they will often assume that the assertion itself is invalid and simply remove it. Comments help preserve your assertions.
  • Assertions are in particular helpful during the development of new code, since they quickly point you to erroneous behavior in your code. However, too many assertions make the debug version of OGDF slow and algorithms might scale badly. Therefore, unnecessary assertions have to be removed in the final implementation.

Error handling

Handling of error conditions is done using C++ exception handling. Objects that are thrown as exceptions belong to special exception classes defined in basic/exceptions.h . Exception classes have a common base class Exception, derived classes specialize the kind of exception to be thrown and allow discrimination of exceptions, e.g., InsufficientMemoryException indicates that memory could not be allocated, or AlgorithmFailureException indicates that an internal error in an algorithm has been detected.

It is important to take care that allocated resources will correctly be freed if an exception is thrown. Usually the best and easiest way is to use objects that allocate resources during construction and free them during deconstruction. This forwards the job of freeing allocated resources when an exception is thrown to the stack unwinding process, which is performed automatically when an exception is thrown. A more detailed treatment can be found, e.g., in Bjarne Stroustroup, The C++ Programming Language, third edition, Section 14.4, Resource Management.

There are a few more rules related to error handling:

  • Make it hard to ignore error conditions of functions. Do not bury error codes in return values or code them into output parameters.
  • Never use exit() or abort() to terminate the application if an error occurs. If errors occur they have to be handled. Always bare in mind that your algorithms or functions are called from an application which keeps (usually unsaved!) important data. If you call exit() in your function, these data are lost and the application behaves in an intolerable manner to the user.

General recommendations

The following rules have to be followed in order to prevent subtle and hard to find bugs.

  • Use inline functions instead of macros where possible. This is as efficient but provides stronger type checking and avoids subtle bugs caused by simple textual replacement.
  • Never cast away const from a variable.
  • Do not reference memory that you do not own, or that you have freed.

Making code more readable improves its maintainability; the following recommendations help to achieve this goal:

  • Use constants or enumerations instead of defines. Avoid magic numbers, use meaningful constants instead.
  • In declarations, place the * for pointers and & for references directly before the variable. Though these symbols actually belong to the type, placing them directly after the type can be misleading if several variables are declared, e.g.,
    // misleading: string2 looks like a string but is only a character!
    char* string1, string2;
  • Do not use NULL for a null-pointer (this is a relict from C), use 0 instead.
  • Test pointers explicitly if they are 0, e.g., instead of if(p) write if(p != 0).
  • Use parentheses to clarify the order of evaluation of operators in an expression.

The following guidelines deal with efficiency of code.

  • If possible use initialization instead of assignment for non built-in types. This saves an unnecessary default constructor call.
  • Minimize the number of temporary objects that are created as return value, or as arguments to functions.
  • Tight C++ does not guarantee efficient machine code. Do not use bizarre expressions just to make your code fitting into a single line.

The following recommendations improve the usability, robustness, and maintainability of code.

  • If your code allocates memory, add debug-only code to set the uninitialized contents to a known but obviously garbage state. Setting memory to a consistent value will make it easier to find and reliably reproduce bugs that use uninitialized memory.
  • If your code releases memory, first destroy its contents so that you have no valid-looking garbage hanging around.
  • Always look for, and eliminate, flaws in your interfaces. Anticipate how programmers will call your functions. Does the “obvious” approach work?
  • Do not write multipurpose functions. Write separate functions to allow stronger argument validation. Make sure that your functions are readable at the point of call. Each function should perform one task and its arguments should make the meaning of the call clear. The presence of true and false arguments often indicates that a function is doing more than one task, or that it is not well designed.
  • Use strict definitions for inputs of functions to maximize the effectiveness of your assertions. Do not silently accept oddball values of inputs just because you can, this hides bugs rather than finds them. Assert that input parameters are meaningful instead.
  • Do not pass data in static or global memory.
  • Strip undefined behavior from your code, or use assertions to catch illegal uses of undefined behavior. Asserting for undefined behavior prevents programmers from abusing unspecified details of your implementation.
  • Do not write parasitic functions that rely on internal workings of other functions.
  • Either remove implicit assumptions (e.g., assumptions about the system architecture like the size of a word or alignment rules), or assert that they are valid.
 
tech/cs_progcon.txt · Last modified: 2010/09/30 10:19 (external edit)
This page is driven by DokuWiki