Improve Your Programming with Asserts

Asserts help you track and fix bugs faster, provided you understand how to use them effectively. Bruce offers ten easy rules for creating more reliable software with asserts.


December 01, 1997
URL:http://www.drdobbs.com/tools/improve-your-programming-with-asserts/184410341

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

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).

Do not assert FALSE. You should not assert unconditionally. Include the test for which you are asserting in the assert itself. Instead of Example 3(a), your code should read like Example 3(b).

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 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.

DDJ


Copyright © 1997, Dr. Dobb's Journal

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

Improve Your Programming with Asserts

By Bruce D. Rosenblum

Dr. Dobb's Journal December 1997

(a)
h = GlobalAlloc(size_t, flags);
if(h ++ NULL)
   ASSERT(h != NULL, "Allocation failed");
else
   DoThis(h);
(b) 
m_pBtn = new CButton ();
ASSERT(m_pBtn != NULL, "Button not instantiated");

Example 1: (a) Do not assert memory errors. (b) Code to avoid in source files.


Copyright © 1997, Dr. Dobb's Journal

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

Improve Your Programming with Asserts

By Bruce D. Rosenblum

Dr. Dobb's Journal December 1997

m_bmpSplash.LoadDIBitmap ( IDB_SPLASH );
ASSERT( m_bmpSplash != NULL, "Bitmap not loaded");

Example 2: Do not assert resource failures. This code is incorrect because the error will not be handled when it occurs in the ship version.


Copyright © 1997, Dr. Dobb's Journal

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

Improve Your Programming with Asserts

By Bruce D. Rosenblum

Dr. Dobb's Journal December 1997

(a)
if ( m_hinst == NULL )
  ASSERT( FALSE, "Null Handle");
(b)
ASSERT( m_hinst != NULL, "Null Handle");
(c)
switch ( i )
{
  case 1:
    DoSomething ();
    break;
  case 2:
    DoSomething2 ();
    break;
  default:
    ASSERT ( FALSE, "Unknown switch case" );
}

Example 3: Do not assert FALSE. Do not use (a), use (b) instead. If you have a switch statement that has no default case, it should be unconditionally asserted like (c).


Copyright © 1997, Dr. Dobb's Journal

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

Improve Your Programming with Asserts

By Bruce D. Rosenblum

Dr. Dobb's Journal December 1997

(a)
VERIFY((z = x * y) < kMaxValue);
(b)
z = x * y;
ASSERT( z < kMaxValue, "z exceeds valid limit");

Example 4: Do not use verify. Instead of (a), your code should read like (b).


Copyright © 1997, Dr. Dobb's Journal

Dr. Dobb's Journal December 1997: Improve Your Programming with Asserts

Improve Your Programming with Asserts

By Bruce D. Rosenblum

Dr. Dobb's Journal December 1997

Figure 1: An enhanced assert.


Copyright © 1997, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.