Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C++


June 1997/Being Assertive in C/C++

Assertions can really raise the reliability of your code, if you use them properly.


Introduction

Although assertions have been around for a long time, it seems that many C/C++ programmers do not habitually use them. I think that is unfortunate, and in this article I hope to encourage other programmers to write assertions more regularly. The team of which I am a member implements editors, interpreters, and compilers that support a visual programming language. Some of us use assertions regularly and have found them to be an extremely effective way to detect and pinpoint bugs. Using assertions, we find these defects early in the development process, when it is almost always easier to understand and fix them. Besides bug-hunting, assertions have also turned out to be versatile for other purposes. In this article I describe some of those other uses for assertions as well.

Note: all listings are from actual project code, with only minor editing on my part along with deletion of code irrelevant to the discussion. In other words, this is working code, and it isn't always pretty. But I hope that it will suffice for illustrating the various ways in which assertions can be applied.

Simple Uses of Assertions

Assertions can be used to check for valid arguments at the start of a function, to check that an index is within range before applying it to an array, to determine whether the usage of an object is consistent, and to validate post-conditions when exiting a block of code.

Some of these usages are illustrated in Listing 1. In function AddCode the first argument is a pointer, which should not be null, so we check it with an assertion before using the pointer. Validating function arguments is a natural and effective way to apply assertions; a single function comprises a natural unit for thinking about coding logic. I find that writing assertions at the start of a function clarifies and makes explicit the assumptions that might otherwise remain vague and unconscious. Doing so leads to code that is more correct and more robust.

In function MarkSelector, the assertion checks that SelectorIndex is within a valid range before it's used as an index into SelectorArray. Thus the function protects itself against stepping off the end of the array and trashing memory.

The design of class CodeSection (immediately below MarkSelector) requires that member function SetContentSize be called before any call to member function GetContentSize. The assertion in GetContentSize checks whether this requirement has been met.

Assertions can be used to check post-conditions at the end of a function. Member function PipePrepare::Finish (Listing 2) provides an example of this usage. After this function has executed, all five members of the PipePreparer object should have the value INVALID_HANDLE_VALUE. Most of those data members will have been set by the function itself, but some are set by other member functions. We can therefore say that the post-conditions actually apply to a sequence of calls that culminate in the current function. Testing post-conditions thus helps to verify and document logic that involves multiple functions calls.

Other Uses of Assertions

Checking Data Structure Validity

Assertions can be used to check that the contents or organization of a data structure is valid. A typical example is after initialization at program startup. In Listing 3, every element of the static array CodeGenerator::NamesTable should contain some non-null pointer to char after member function InitNames has completed. The loop at the bottom of InitNames with the assertion as its body ensures that that is true.

For a small table this would be a trivial and unconvincing example. The actual table, however, contains about three hundred entries. Moreover, new entries were added as development proceeded, and it was possible to get one wrong. I did in fact make this mistake. While editing, I added an enumerator to the enum, then went into InitNames and copied and pasted an existing assignment into the sequence of assignment statements. But I forgot to edit the index to be the new enumerator, so one entry in the table was being initialized twice and the new entry was not being initialized at all. As soon as I compiled and ran the program, the startup code called InitNames and the assertion failed. The mistake was obvious because the assertion was close to where the error was and because I had just made the error!

Detecting Forbidden Function Calls

Listing 4 shows a base class T_object with a virtual member function IsLinkedIn. Ten derived classes are defined, with four of those classes overriding the definition of IsLinkedIn. The remaining derived classes inherit IsLinkedIn from T_object. The code for our project contains many pointers of type T_object*, but in almost all cases they point to objects of derived

classes. The member function IsLinkedIn only makes sense for a few of those classes. Complex data structures contain pointers to objects of different derived classes, so a coding error could easily cause IsLinkedIn to be called on an object of an inappropriate type. The assertion in T_object::IsLinkedIn detects such errors.

Detecting Unimplemented Functionality

During the course of development we sometimes design and implement the common cases for a piece of functionality but defer working up the rarely occurring cases until later. Function PushVarCString in listing 5 takes an argument InObject and extracts a C string, placing the result in a buffer whose address is passed back. The caller also passes in a buffer together with the size of that buffer. There is no limitation on the length of the C string. If the string is not too long, the caller's buffer is used. Otherwise, the function must allocate memory elsewhere and use that as the buffer instead. In almost all cases the caller's buffer is sufficiently large, so we deferred creating the mechanism for the uncommon situation. Nevertheless, an extra long C string would occasionally come through. When that happened, the assertion would fail, letting us know that the case was not being handled properly, and reminding us that there was a loose end that needed to be dealt with.

Checking Platform-Specific Code

Much of our code is written for different platforms, including the code that hooks into memory management. One implementation of memory management is pointer-based, another is handle-based. The latter uses two levels of indirection and allows for compaction of memory. The former does not. Listing 6 shows the function CompactMemory, whose source code is shared across platforms. Conditional compilation is used to determine what CompactMemory actually does. If the macro HANDLE_BASED is defined as 0, the platform uses a pointer-based memory manager and the compaction routine should never be called. The assertion fires should such an error occur. This happens, for instance, when different files are compiled with the macro HANDLE_BASED set inconsistently.

Save Assertions for the Unexpected

Assertions should be used to trap for conditions that are never supposed to happen. In such usage, if the code is completely correct then no assertions will fail. They will fail only if there is a logical error in the code. By contrast, a condition such as running out of memory on the heap is not a logical error. Memory exhaustion is a fact of programming life. It will occur repeatedly, and the software should be designed to take care of it.

An assertion is not adequate for dealing with abnormal but expected conditions — unless a cryptic error message followed by program termination is acceptable to users of your software. On the other hand, what is inappropriate in a released product can be useful for detecting error conditions during development. Running out of memory is one such condition. Another is shown in listing 7. Class IC_Instructions contains a fixed-size array of IC_Instructions to which instructions are added by invoking member function AddInstr. When the array is exhausted (and perhaps an attempted reallocation fails as well), the problem should be handled in a graceful way and the user informed. As a stopgap during development, we placed an assertion to detect and notify developers of the problem on those rare occasions when it occurred.

Don't Forget Defensive Programming

In his book Writing Solid Code Steve Maguire enjoins, "Don't hide bugs when you program defensively." [1] He is saying that defensive code that quietly handles "impossible" conditions keeps you from becoming aware of those very conditions — therefore you should supplement the code with assertions to raise a red flag.

I would like to turn that argument around: just because you are using assertions, don't forget the defensive programming. Assertions are a great development tool, but for performance and other reasons they are often disabled in the code used to build the released product. Even if they are left enabled, assertions usually have undesirable behavior. Assertions as usually implemented will fail either by abruptly terminating the program or continuing execution as normal. Abrupt termination is not too user-friendly. Continuing normal execution may lead to a program crash or worse, which is not too user-friendly either. Defensive coding provides some checks and firewalls to help a program degrade gracefully.

Function ObjectToInt (Listing 8) shows two instances where assertions and defensive programming can be used together. At the beginning of the function the if statement prevents the dereferencing of a null pointer. In the default case within the switch statement the code assigns IntValue to a known value rather than return whatever uninitialized value the variable would otherwise contain.

Assertions Are Code

Although most assertions are simple and written clearly, on occasion they may be wrong and need to be debugged. Also, as development proceeds, design and implementation can change so that expected conditions and requirements no longer hold. Assertions can become obsolete.

Like any other piece of code, complicated or unclear assertions should be documented [2] . Suppose you write an assertion that anticipates a problem for which you know the solution. Then it's a good idea to include a comment in the code that captures your understanding of the situation.

Because executing an assertion requires at a minimum evaluation of an expression, assertions can entail significant performance overhead. Since assertions are typically implemented as macros, they can easily be disabled by expansion into a trivial expression that incurs insignificant, or no, run-time overhead. I have the impression that in practice, assertions are disabled in released code out of such concern for performance. However, we shipped one of our products with assertions active, and the effects on performance were not notieable.

Finally, consider rolling your own assertion rather than using the standard definition. In our project we use assertions that continue with program execution rather than terminating. This allows team members to focus on their own tasks and not be forced to fix defects better left for someone else or for a later time. I also like program execution to go on so I can see how robust the code is — it is always interesting to see how far the software goes before it crashes!

Closing

I've worked in C for about six years and C++ for the past two. I did not start using assertions until about two years ago, and now I'm simply amazed and dismayed that I waited so long. The payoff has been incredible. I am convinced that the quality of our software is far better as a result of using assertions. Some of the bugs we have found would otherwise still be lurking in the code, waiting to bite us.

Acknowledgement

My thanks to Garth Smedley for reviewing and commenting on a draft of this article, and especially for authoring many of the assertions in the code examples.

References

[1] Steve Maguire. Writing Solid Code (Microsoft Press, 1993), p. 33.
[2] ibid, pp. 21-23.

Further Reading

The chapter entitled "Assert Yourself" in Writing Solid Code presents an excellent discussion of assertions, which served as partial inspiration for this article. I highly recommend the entire book as required reading for any working programmer.

Lloyd Chambers' "Simple Yet Effective Bug Detection," MacTech Magazine, November 1996, describes creating variants of assertions and how to use them to trap errors in system calls and to detect memory leaks.

For a different view, see Jack Reeves' article "Exceptions and Debugging," in C++ Report, November-December 1996. He states, "Programmers overwhelmingly prefer exceptions to using asserts." He has not changed my mind, but that could be because I am still climbing up that steep C++ learning curve.

Earl Fong is a software developer with Pictorius Inc. in Halifax, Nova Scotia. He has an M.S. in Computer Science, enjoys programming immensely, and wonders if he will still be doing this for a living twenty years hence. Comments on this article are welcome at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.