Using Assert for fast C++ Development

January 18th, 2006 by Andrew Skowronski - Viewed 22069 times -




What is an “assert”
The assert() function is part of the C standard library. It permits you to break into the debugger when a condition that you don’t expect occurs, and is only active in the debug configuration. In the “ship” binaries, that end-users will see, the assert acts as an empty macro and does absolutely nothing. On windows, if an assert fails, you will see a message box with a “retry” button that lets you get into the debugger.

It is considered good practice to sprinkle all C++ code with asserts. But as a very brief overview a typical function might contain the following sorts of asserts:

  • Confirm expected inputs (e.g. that an input string or array is not empty)
  • Confirm expected situations during the algorithm (e.g. confirm that a local variable is holding a valid value)
  • Confirm “post” conditions (e.g. that the algorithm did what is was supposed to)

The following is a bit of a “contrived” example, but it shows these three different types of asserts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Duplicate string multiple times
CString DuplicateString( int arg1, CString & arg2 ) 
{
   // confirm rules for the expected inputs
   assert( arg1 > 0 ) ;
   assert( arg2.IsEmpty ) ;
 
  CString retVal = "" ;
  for ( int i = 0 ; i < arg1 ; i++ )
  {
    retVal = retVal  + arg2 ;
 
    // Check for unexpected situation during the algorithm
    assert( !retVal.IsEmpty() ) ;
  }
 
  // Post condition
  assert( retVal.GetLength() == arg1 * arg2.GetLength() ) ;
 
  return retVal ;
}

It is not important to test things that you really really know are going to be valid, but “if in doubt” put an assert.

Asserts are an important maintenance tool, so even if there are no bugs in your code today, you can protect your code from future modifications by adding asserts. For example, your code may crash if the caller passes 0 as an argument. You may know that no caller ever does that. But some “idiot” might add a new call tomorrow that does pass 0. So, if your code includes a built-in check for that condition you might save yourself a lot of debugging and finger pointing.

Asserts and XSI SDK
The XSI C++ SDK is a perfect case for needing asserts. This is because the SDK is designed not to crash. Even if you divide by zero XSI will often catch the error and just mention that your plugin wasn’t well behaved. It is sometimes possible to crash, especially if you corrupt memory, but the overall behavior of the API is to try to keep going, no matter what.

One particular issue with this fail-safe attitude is the fact that if you have a bug you can end up working with an “invalid” object. All the methods will still work but the object won’t really be pointing to a real XSI object. So the methods won’t be doing anything. This can mean that a bug early in a program can lead to head scratching much later when you are trying to figure out why some methods seem to do nothing at all.

As a very basic example consider:

1
2
Parameter p ;
p.PutValue( 4.0 ) ;

This code does nothing at all. The variable p was never initialized to a real XSI object, so setting a value on it has no effect whatsoever.

Here is another example:

1
2
3
4
5
6
7
8
9
10
11
Null AddNullToModelAndGroup( Model & inModel,const CString& inGroupName,const CString& inNullName )
{
	Null newnull ;
	inModel.AddNull( inNullName, newnull ) ;	
 
	Group group = inModel.GetGroups().GetItem( inGroupName ) ;
 
	CStatus st = group.AddMember( newnull ) ;
 
	return newnull ;
}

Now this function is straightforward, but there is a lot that can go wrong. If you call this function but discover in later testing that the Null doesn't actually exist or wasn't added to the group then you will have to scratch your head, do some debugging or add some LogMessage calls to try to find the solution.

However my suggestion is that you should sprinkle assert calls in your code directly as you write it.

Here is the modified function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Null AddNullToModelAndGroup( Model & inModel,	const CString& inGroupName,	const CString& inNullName )
{
    assert( inModel.IsValid() ) ;
    assert( !inGroupName.IsEmpty() ) ;
    assert( !inNullName.IsEmpty() ) ;
 
    Null newnull ;
    inModel.AddNull( inNullName, newnull ) ;	
    assert( newnull.IsValid() ) ;
 
    Group group = inModel.GetGroups().GetItem( inGroupName ) ;
    assert( group.IsValid() ) ;
 
    CStatus st = group.AddMember( newnull ) ;
    assert( st.Succeeded() ) ;
 
    return newnull ;
}

Some example scenarios that these asserts will catch:

  • Invalid arguments. This function will tell you right away if the caller passes empty strings or a bogus Model. If these asserts fail then you can immediately blame the calling code, rather than wondering if AddNullToModelAndGroup is wrong.
  • Invalid group name. One of the most likely failures would be the case where inGroupName doesn't actually correspond to a real group. If assert( group.IsValid() ) fails we know that the model exists, we know that inGroupName is not empty, and we know that there is not group by that name.
  • Failure to create the Null. If the AddNull call fails we'll know about it. Its hard to think of any reason why it would fail, given that the name is not empty, but better safe than sorry.

Here are some other typical asserts you might see in XSI C++ code:

1
2
3
4
assert( x > 0.0 && x < = 5.0 ) ;
assert( mynull.GetName() == L"MyNullName" ) ;
assert( points.GetCount() > 0 )  ;
assert( myref.IsA( siParameterID ) ) ;

Tips

  • You may want to create your own assert macro, possible with a shorter name if you don't like typing a lot.
  • You may also want to create macros that are shortcuts to testing CBase::IsValid and CStatus::Succeeded as these will be the most common asserts that you write.
  • Another useful macro is one to test two floating point values with a "fudge" factor to ignore rounding errors.
  • Asserts are not a replacement for error handling. In some cases you may need both the "debug" and the "ship" version of the code to test input arguments, log error messages or tell the XSI user that they did something wrong. Asserts are meant to test things that should "never" happen as a way of catching bugs, while error handling should cover the bad edge scenarios that "might" happen.
  • You can add extra context strings to your assert, for example assert( x > 0 && "Value read inside foo is wrong" ) ;. The string will appear as part of the message box.
  • If there is code that you believe is never executed, but are afraid to delete it, try putting assert( !"This should never happen. inform so and so immediately") ; inside it. If no one contacts you after testing you can be more confident to delete the code.
  • If you are given complex code that you didn't write, and have to make changes to it, first add a lot of asserts that verify the assumptions that you find. Make sure these asserts are valid by testing, then start changing things. The asserts will help confirm that you really understand the code and that it wasn't actually more buggy than you might have thought.

Conclusion

This is just a very brief introduction to the concept of asserts. You can find lots more details in general (non-XSI) coding practice books (for example in "Code Complete" by Steve McConnell). Its a concept that goes beyond and particular language or program, but it is specifically useful for the XSI C++ API.

I can say this because I've been involved with a large C++ API project recently and I can confirm that using asserts was a good practice for fast debugging and quick stabilization of complex code. We added many asserts and used it to get meaningful information from testers and also quick entry into the debugger when testing our work. It's not the complete solution to everything, and we use other debugging tricks, but it should be part of any robust C++ development project.

One Response to “Using Assert for fast C++ Development”

  1. Hello Andrew!

    Nice article, it would be cool to have another one about unit tests ; )

    Andrea