easy rules for more reliable software
Bruce is vice president of software development at Turning Point Software and Inera Inc. Bruce can be contacted at [email protected]
Several years ago, we were testing the near-final build of a Macintosh product. The program crashed, and one of our quality-assurance engineers found herself staring at a Macsbug screen. When I sat down to try to locate the problem, I couldn't get stack crawl information.
The tester went to another machine to try to reproduce the crash with the debug build of the application. Meanwhile, I tried to track the problem on her computer. Because I didn't know whether the problem was reproducible, I didn't want to destroy the state of the machine that had crashed. After hours of sleuthing with the Macsbug data and a set of source code, I thought I was getting close to the problem, but I still had no answers.
Four hours later, the engineer came back and said she had triggered an assert message that indicated memory corruption. The assert also showed a stack trace that pointed to the suspect subroutine. I took one look at the source code for the suspect routine and immediately saw the source of the memory corruption. In one minute, the assert allowed me to find a problem that four hours of sleuthing had failed to uncover.
Many People Don't Understand Asserts
Effective use of asserts will help you track and fix bugs faster, keep your project on schedule, and ship a bug-free application. Because they slow execution, however, it is important to maintain debug and ship versions of your application during the entire development cycle. Debug builds include asserts and ship versions do not, allowing your application to run at maximum speed. Users of your application never see the debug version that you and your quality-assurance engineers have tested, but they will appreciate the added stability from the correct use of asserts.
Our experience conducting code reviews has shown that developers need guidance in the appropriate use of asserts when they first start to use them. At Turning Point Software, we have developed ten guidelines for the use of asserts. These guidelines explain correct use of asserts. More importantly, they explain incorrect use of asserts. Incorrectly used asserts can, in many cases, be worse than no asserts at all.
There is one important point to stress before reviewing the guidelines: Asserts must never be used as a substitute for correct error handling. The most common mistake that engineers make is to use assert where a legitimate error can occur. Asserts are only used to indicate an impossible condition that should never happen in normal operation of the application. If the potential situation you are checking can happen in the shipping application (such as a memory or file failure), it must be handled with appropriate error handling (returning an error code or throwing an exception, depending on how errors are handled in your specific project). The following guidelines are based on this fundamental concept.
Assert explicit programmer errors. Asserts are best used to signal impossible conditions, those that should never occur in the application if there are no mistakes in programming. For example, in a well-behaved application, if a memory block of 1000 bytes is allocated, the code should never read or write from location 1001 of that memory block.
Because such memory overwrites are a frequent cause of nasty bugs, allocate an extra byte at the end of each memory block and set it to a magic value (not 0 or -1; 0x6A is a nice value). Every time the block is referenced, check to see if this value has changed. If it has, the end of the block has been overwritten and an assert can signal this error immediately. If you do not assert, you will not know of a problem that could corrupt memory and be very difficult to track.
Assert public API functions. Every parameter of an API that is exposed to external callers should be asserted if possible. For example, if a function takes a pointer that should never be NULL, assert that it isn't. If a function takes a long containing flag bits, assert that only valid bits are set (that is, if only 18 bits are used, assert that unused bits are still 0; if they aren't zero, an incorrect value has been passed). If a parameter should be an enum in a small range, assert that the value falls within the range. For every function, if the API is thoughtfully reviewed, you can find a way to assert virtually any parameter.
Assert assumptions. Assert any assumption that you have made in the code. For example, if you write a routine assuming it will only produce positive numbers as return values, assert the return value to make sure it's positive.
Assert reasonable limits. Assert that values are within reasonable limits for the application of the code. For example, when locking and unlocking handles, check the lock count. Assert that the lock count has not exceeded 32, which might be considered a reasonable number of locks. If this limit is exceeded (even though it is within the limit of the operating system), you may have a logic problem in your application code. A similar check can ensure that the lock count never falls below zero.
Assert unimplemented and untested code. If you provide a function entry point, but have not had a chance to fill in the code yet, you should assert it. If someone else calls this function before the code is completed, she will know why it isn't doing the correct thing.
If you write code you haven't yet had a chance to test, you should assert to warn a caller that the code may not work. If you test only some cases of the code, assert those cases you haven't tested. For example, I once used a library from another developer to manage some special arrays. It was written so that it could handle a physical array size up to 64 KB. The author, however, never expected the array size to exceed ten KB and never tested past that point. As soon as the array grew past 32 KB, lots of problems surfaced. A simple assert for larger arrays would have forewarned me that I was sailing into uncharted waters.
Assert classes. MFC provides a method in classes derived from CObject called AssertValid. This method can be used to validate the state of a class. If you create a new class, make sure that you add an AssertValid method that validates as many instance variables of the class as possible. You should call this method from all appropriate methods of the class. If the class has been derived from another class, the first thing your AssertValid method should do is call AssertValid in the base class. For example, when using MFC, MyApp::AssertValid() should first call CWinApp::AssertValid().
Do not assert memory errors. Memory errors can happen at run time and should always be handled as failure conditions. They should never be handled with asserts. Example 1(a) should never appear in a source file.
Similarly, the failure of new to construct a C++ object should never be asserted. The new operator allocates memory and therefore can fail at run time no matter how small the size of the object. Example 1(b) should never appear in a source file.
You also should not assert new because, in most new compilers, new will throw an exception if the memory allocation for the object fails. In this case, the assert will never be triggered even if there is a memory failure. If m_pBtn is NULL, the exception will pass control to a catch statement, and in this example, the assert is not inside a catch.
Do not assert resource failures. The failure of resources to load is a run-time failure that can happen in the ship version of your application due to lack of memory or a disk failure. It must be handled as a legitimate run-time error. Example 2 is incorrect because the error will not be handled when it occurs in the ship version of the product (and it can happen).
If you need to assert FALSE, you probably are flagging a condition that should be handled with a specific check for a condition. There are only a few exceptions in which it is appropriate to assert FALSE. If you have a switch statement that has no default case, it should be unconditionally asserted; see Example 3(c).
Do not use VERIFY. Many environments include a macro VERIFY such as the one in MFC. This macro allows shipping code to be included inside the check for an assertion condition. This macro should not be used. All validation code that goes into debug-only builds should be in separate statements so that it is clearly separate from the ship version of the code. Using VERIFY is more likely to be error prone when maintaining code than using an assert. Instead of Example 4(a), your code should read like Example 4(b).
A Better Assert
A basic assert displays a message such as "Error!" that tells you something has gone wrong. While useful, this simple assert can easily be made much more informative. The additional information can save you time tracking the source of the assert.
I once spent several hours tracking a problem because the debug version of the application for which I was writing an extension simply put up a message "Assert," telling me I had done something wrong. I finally tracked the problem to a handle that hadn't been unlocked before calling free. A verbose assert would have told me the source of the problem in ten seconds.
To avoid such problems, we use an enhanced assert that lets you track bugs faster. (The complete source code for this assert is available electronically; see "Availability," page 3.) What is "enhanced" about this assert?
- The assert is verbose. As Figure 1 shows, a clear Reason for the assert is displayed instead of a generic message. This allows you to understand the problem without referring to the source code.
- The source FileName and Line # are shown to easily determine where the assert was triggered.
- Additional Memory information can be displayed in the assert. In this case, the value of the offending handle is shown. Such information can be helpful in evaluating the cause of the problem.
- The Stack Crawl is shown, which helps find the true cause of the problem more rapidly. The code needed to display the stack crawl often takes more advanced system-level expertise to write, but the time invested to develop this code more than pays for itself in reduced debugging time.
- The Display Asserts check box allows asserts to be turned off to reach a specific portion of the application. They can be reenabled from a menu command.
- Display As Hex lets the display of Memory data be switched between decimal and hex mode for easier debugging.
- The Debugger button transfers application control to the appropriate low-level debugger.
- The Exit to Shell button terminates the application immediately.
- The OK button lets users proceed with application execution.
The Better Assert program was written for the Macintosh. We've tested the code with CodeWarrior 9; it may need small modifications for Symantec or MPW compilers. It has been designed to avoid memory allocations in calls to _tdb_DoAssertDlg, the core assert display function. This technique helps to avoid disturbing the problem you are observing.
You can implement the same functionality on Windows. The include files are cross-platform and have conditional compile information for Windows. Most of the functionality, except for the stack crawl, is not difficult to write for other platforms. Code for the stack crawl is provided only for 680x0 Macintoshes.
This code includes a small CodeWarrior project that illustrates the simplest use of this library. The following are quickstart instructions to use this code in your own projects. More extensive documentation can be found in the file TPSDEBUG.H.
- Define MAC and TPSDEBUG for your project. Under Project Settings, 68K Linker options, turn on Generate A6 Stack Frames.
- Add the files DBGGLBL.C, MACDEV.C, MACDLG.C, MACSTACK.C, and TPSDEBUG.C to your project.
- Call tdb_StartupDebugLib and tdb_ShutdownDebugLib during your application startup and shutdown, respectively.
- Add the following three lines to all of your source modules after all system include files and before any project-specific include files:
#include "tpsdefs.h" #include "tpsdebug.h" ASSERTNAME
- Call the assert macros liberally in your code. The basic macro is TpsAssert (NULL != pointer, "Invalid Null Pointer");. There are additional macros defined in TPSDEBUG.H that can be used to show additional information in the assert dialog. For example, TpsAssertB dumps an array of bytes at a specified location. This feature allows you to examine the data that triggered the assert. There are also macros for dumping arrays of words, longs, and strings.
- Other useful macros include UnimplementedCode and UntestedCode. These can be used to warn others of code that is not ready for prime time.
- Whenever you make major builds of your application (such as quality assurance, major milestones, gold release), make two builds: one with TPSDEBUG defined (your debug build) and one with TPSDEBUG removed (your ship build). Quality-assurance staff can test both the debug and ship builds; your customers will receive the ship build.
Copyright © 1997, Dr. Dobb's Journal