Assertions parameterized by location

Many assert macros label the line where the assertion occurred. If the assertion is fired during a program run, this causes an alert to be raised mentioning the exact source line of the assert statement. Usually, this is exactly what should happen, but sometimes a subroutine would like to establish a contract with its caller that if a certain condition is not met, it is the caller that should be identified in the assertion!

For example, imagine if you are writing a routine whose job it is to hang up the phone, with the precondition that it is currently not already hung up:

void Phone::HangUp()
   {
   assert(this->status == Phone::statusConnected);
   ...
   }

There may be many places where this method is called. Yet no particular call site will be indicated in the assertion message. A manual inspection of the call stack with the debugger is the only way to uncover the guilty caller who is hanging up an already-hung-up phone. Using the default assertion libraries, there is no simple way to log the actual origin of the problem.

As a mechanism for addressing this problem, I use my own Assert() routine and an abstraction for encapsulating the idea of a source location in a file. I call it SLOC (for Source LOCation), and it may be freely passed as a parameter to any routine you like. For example, if you wanted Phone::HangUp()’s assertion to be tied to a caller’s location, you’d write:

void Phone::HangUp(SLOC sloc)
   {
   Assert(sloc, this->status == Phone::statusConnected);
   ...
   }

Then in the caller, you can write:

phone.HangUp(SLOC (__FILE__, __LINE__));

Because I like brevity, I use the abbreviated macro “_d_”:

phone.HangUp(_d_); // _d_ means source location "D"efinition

This way, when the assertion is reported by the program, it will make reference to the passed-in location, rather than the location of the Assert() in the code that’s reporting the error. Source locations can be applied in many debugging scenarios, for instance if we wanted to track the previous disconnection call we could do so:

void Phone::HangUp(SLOC sloc)
   {
   // this->slocLastHangupCaller contains the last
   // caller's location.  assert message could include it,
   // or just inspect from debugger
   Assert(sloc, this->status == Phone::statusConnected);
   ...
   this->slocLastHangupCaller = sloc;
   }

By checking slocLastHangupCaller, you will be able to find the source of the previous (unexpected) hang-up!

You might theorize that run-time access to the stack is the ultimate API for dealing with this sort of thing. Imagine if PHONE::HangUp() could somehow obtain an object representing the call stack, and then extract whatever information it wanted about the callers. That’s a little heavy-handed, and I believe that the odds of the API being abused are so high that a “narrower” protocol agreement between callers and subroutines would need to be established for the common scenarios.

At the end of the article, I’ve given sample code for those interested. This includes an additional template class called TR<[type]> (for TRacker), which lets you audit the places in your code where certain assignments are made. If the value of a variable isn’t what you expect, it automatically identifies the location of the previous assignment! So for instance:

class Phone 
  { 
private:
  TR<bool> connected;
public:
  Phone::Phone () :
       connected (_d_, false, "connected")
    {
    // set up phone
    ...
    }
  void Phone::Call(SLOC sloc, PhoneNumber number)
    {
    // assert that we are not already connected
    // (automatically reports location of last assignment if we are)
    connected.AssertValue(sloc, false, "false");
 
    // call the number
    ...
 
    // set to true and record that we are doing so
    // on behalf of the caller
    connected.Set(sloc, true);
    }
  void Phone::HangUp(SLOC sloc)
    {
    // automatically reports location of last assignment to false
    // *if* connected is not true.
    connected.AssertValue(sloc, true, "true");
 
    // hang up the phone
    ...
 
    // records the caller as the location of false assigment
    connected.Set(sloc, false);
    }
  };

It should be apparent why why this is useful. These mechanisms are extremely helpful in finding bugs in systems, and are very addictive once you start using them. To further simplify matters, I’ve also created a stock type for TR as TR_bool, to reduce typing. Bear in mind that if you want the default behavior of an assertion reporting its own line, it’s as easy as:

Assert(_d_, "report the error here.");

Here’s the small amount of code, which you can adapt to your needs. Please report any problems:

  1. location_parameterized_assert.cpp : view CPP

Tags:

Leave a Reply


Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported
Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported