Symbian Developer Network

View a printable version of the current page.
  Wiki > Symbian Developer Network Public Wiki > ... > Code Clinic > Code Clinic December 2008 - ASSERT yourself
  Code Clinic December 2008 - ASSERT yourself
Added by Jo Stichbury, last edited by Jo Stichbury on Nov 28, 2008  (view change)
Labels: 

Introduction

When code encounters a bug it should stop executing, to highlight the problem at the point at which it occurs. Halting running code as soon as a problem is detected, rather than allowing code to continue executing, makes it easier for you to track bugs down and fix them. This is preferable to either allowing the code to continue running, such that the problem 'propagates' and makes bugs hard to find, or to even ignore the bug entirely and 'code around' it.


 
A good way to detect bugs in C++ is to use assertion statements. An assertion checks that the state of objects, function parameters or return values, is as expected. Typically, an assertion evaluates a statement and, if it is false, halts execution of the code. Consider assertions as an annoying colleague, leaning over your shoulder pointing out defects for you as your code runs. Assertions don't prevent problems, but make them obvious so you can fix them.

On Symbian OS, you'll find the definition of two assertion macros in e32def.h. The __ASSERT_ALWAYS macro performs an assertion check in both debug and release builds, while the __ASSERT_DEBUG macro checks code in debug builds only, and has no effect in release builds.

// For debug and release builds
#define __ASSERT_ALWAYS(c,p) (void)((c)||(p,0))
...
// For debug builds only
#if defined(_DEBUG)
#define __ASSERT_DEBUG(c,p) (void)((c)||(p,0))
#else
#define __ASSERT_DEBUG(c,p)
#endif

Parameter c is a conditional expression that returns a true or false value. Parameter p is a function, called if c is false, which should halt the flow of execution. In effect, the code behaves as follows:

if (!c)
  p

You'll notice that the assertion macros do not panic by default, but allow you to specify the code to run should the assertion fail. My advice is always to use a panic in an assertion statement (or write a custom function that first logs the specific failure to file and then panics). This is because you should immediately terminate the running code and flag up the failure, rather than return an error, leave or do nothing. Assertions help you detect invalid states or bad program logic so you can fix your code as early as possible. It makes sense to stop the code at the point of error, thus forcing you to fix the problem (or remove the assertion statement if your assumption is invalid). If the assertion simply returns an error on failure, not only does it alter the program flow, but it also makes it harder to track down the bug.

__ASSERT_DEBUG

Here's one example of how to use the __ASSERT_DEBUG macro provided by Symbian OS:

void CExampleClass::TestValue(TInt aValue)
  {
  #ifdef _DEBUG
  // Define panic literal in debug build only to avoid a compiler
  // warning, since it's used in the debug assertion only
  _LIT(KPanicDescriptor, "TestValue"); // Literal descriptor
  #endif
  __ASSERT_DEBUG((aValue>=0), User::Panic(KMyPanicDescriptor,
                                              KErrArgument));
  ...
  }

Of course, this is somewhat awkward, especially if you expect to use a number of assertions to validate your code, so it's sensible to define a panic utility function for your module, as you may recall I did in last month's Code Clinic which discussed panics in detail:

enum TExampleEnginePanic
  {
  ECorrupt,          // =0,
  ENotInitialized,   // =1,
  EInvalidTestValue, // =2
  };

static void ExamplePanic(TExampleEnginePanic aCategory)
  {
  _LIT(KExamplePanic, "EXAMPLE-ENGINE");
  User::Panic(KExamplePanic, aCategory);
  }

The assertion in TestValue() is now written as follows:

void CExampleClass::TestValue(TInt aValue)
  {
  __ASSERT_DEBUG((aValue>=0), ExamplePanic(EInvalidTestValue);
  ...
  }

The advantage of using an identifiable panic descriptor and enumerated values for different assertion conditions is traceability, for yourself and callers of your code, when an assertion fails and a panic occurs. This is particularly useful for others using your libraries, since they may not have access to your code in its entirety, but merely to the header files. If your panic string is clear and unique, they should be able to locate the appropriate class and use the panic category enumeration to find the associated failure, which you will have named and documented clearly to explain why the assertion failed.

There may be cases where there's nothing more a client programmer can do other than report the bug to you, the author of the code; alternatively, the problem could be down to their misuse of your API, which they'll be able to correct.

ASSERT()

If you don't want or need an extensive set of enumerated panic values, and know that external callers will never need to trace a panic, you may consider using a more lightweight assertion macro. A good example of where this may be appropriate is when you want to test the internal state of an object, which could not possibly be modified by an external caller, and thus should always be valid unless you have a bug in your own code. In these cases, you may consider using the ASSERT macro, defined in e32def.h as follows:

#define ASSERT(x) __ASSERT_DEBUG(x, User::Invariant())

In debug builds only, if condition x is false, User::Invariant() is called, which itself panics with category USER and reason 0. The macro can be used as follows:

ASSERT(iPointer>0);

Personally, I like this macro because it doesn't need you to provide a panic category or descriptor. Some Symbian developers consider it to be infuriating, because it allows you to scatter assertions throughout your code with no easy way to diagnose what went wrong. While it's OK to do so if you're sure you're going to be the only developer that sees them fire, and immediately fix the code accordingly, if they do make it 'downstream', they make problems harder to trace. I think it's better to use them than nothing at all (which may be the case if you have to go back and add supporting code to use __ASSERT_DEBUG()) but I include the following caution here for completeness.

When adding an assertion, consider whether anyone else will ever see it. If so, make it traceable.

As an alternative to using ASSERT to test the internal state of your object, you may wish to consider using the __TEST_INVARIANT macro.

Avoid 'Side Effects'

Don't put code with side effects into assertion statements that execute in debug builds only. By this, I mean code that is evaluated before a condition can be verified.

For example:

__ASSERT_DEBUG(FunctionReturningTrue(), Panic(EUnexpectedReturnValue));
__ASSERT_DEBUG(++index<=KMaxValue, Panic(EInvalidIndex));

The reason for this is clear; the code may well behave as you expect in debug mode, but in release builds the assertion statements are removed by the preprocessor, and with them potentially vital steps in your programming logic. Rather than use the abbreviated cases above, you should perform the evaluations first and then pass the returned values into the assertion.

In fact, you should follow this rule for both __ASSERT_DEBUG and __ASSERT_ALWAYS statements, despite the fact that the latter are compiled into release code, because, while you may initially decide the assertion applies in release builds, this may change during the development or maintenance process. You could be storing up a future bug for the sake of avoiding an extra line of code.

__ASSERT_ALWAYS: Should I Use Assertions in Production (Release) Code?

It goes without saying that your code should be tested thoroughly. A user, or another software developer using your code, expects you to have debugged your code before release; they don't want to do it for you. The use of assertion statements in debug builds can help you detect programming errors inside your code and fix them before delivery.

So why would you need to use assertions in production code? Why does Symbian C++ have an __ASSERT_ALWAYS macro to execute checks in both debug and release builds? For one thing, won't the impact on the execution speed and code size of adding extra checking to your code be prohibitive? Doesn't the outcome of a failed assertion make for a poor user experience when it results in an application's untimely end? Should you ever use __ASSERT_ALWAYS?

First of all, consider what __ASSERT_ALWAYS is checking. You've used debug assertions to verify your internal code logic, so the only reason for using release build assertions is to check the validity of incoming parameter data. While invalid input is a bug from the perspective of your code, it may be caused by an exceptional condition in the calling code, which that code can handle gracefully if you return it an error or leave instead of firing an assertion and terminating the running code.

Let's consider a simple example. Your code offers a method to open and write to a file; the caller passes in the full file name and path, as well as the data to be written to the file. If the file does not exist, it is generally more appropriate to notify the caller through a returned error code or leave value than to assert in a release build. The calling code can then deal with the error.
The following code illustrates this approach, by returning an error if the caller passes in an invalid parameter. (You'll notice that I've included an __ASSERT_DEBUG statement to verify internal state in my code and catch any defects - such as attempting to use the file server handle before it has been initialized).

TInt CTestClass::WriteToFile(const TDesC& aFilename, const TDesC8& aData)
  {
  TInt r = KErrNone;
  if (KNullDesC8==aData)
    {// No data to write - invalid!
    r = KErrArgument;
    }
  else
    {
    RFile file;
    __ASSERT_DEBUG(iFs, Panic(EUninitializedValue));
    r = file.Open(*iFs, aFilename, EFileWrite);
    if (KErrNone==r)
      {   // Only executes if the file can be opened
      ... // Writes aData to file, closes file etc
      }
    }
  return (r);
  }

If the caller is passing data from a source that it does not directly control, say from the user's keystrokes or from a communications link, there is always a possibility for invalid input. In the example above, this could perhaps be handled by creating the file or using a default set of data. The calling code is far better placed to make a 'decision' about how to handle each scenario, so it's better for the code above to indicate the invalid incoming data by returning an error or leaving than by asserting and panicking when the input is invalid.

When illegal input values can only have arisen through a bug in calling code, and can potentially result in 'bad things' occurring, such as data corruption, it is desirable to use __ASSERT_ALWAYS checks. You could still return an error to the caller, but it's probably clearer to flag up the problem so it can be fixed. A good example of this is in the Symbian OS array classes (RArray and RPointerArray), which have __ASSERT_ALWAYS guards to prevent a caller passing an invalid index to methods that access the array. The class provides functions to determine the size of the array, so if a caller attempts to write beyond the end of the array, it can only be doing so because of a bug. The Symbian OS descriptors are similarly protected to prevent writing outside of the descriptor's data area.

__ASSERT_ALWAYS can be used to protect against illegal input that can only ever have arisen through a bug in the caller. The assertion should cause a panic to indicate to the calling code that they have a programming error which needs to be resolved. Once the bug is fixed, the code runs and the failed assertion is never seen by the user.

__ASSERT_ALWAYS should not be used to protect against invalid input that occurs because of an error or exception, since an assertion failure will be seen as a panic by the user, resulting in a poor experience as we discussed in last month's Code Clinic.

More About the Code Clinic

The Code Clinic will feature regularly on Symbian Developer Network. Got a Symbian C++ question? Send it to the Code Doctor at SDN@symbian.com. Questions used will receive a complimentary copy of the Symbian Press book "Developing Software for Symbian OS, Second Edition".

Reviewers

Many thanks to Hamish Willee and Mark Shackman for taking the time to review and provide feedback to this article.

Your Comments

Please feel free to get involved with the Code Clinic, either by editing this article directly, or by posting comments to it. Do you use assertion statements, and if so, how?