Introduction
How often have you found yourself in the situation that you are debugging some code and everything seems logical but it just doesn't work? Quite often as a beginner. Well, I found myself in this situation much less these days because of some great tips I have picked up. I want to share them with you'll and hope they will help you too.
Background
I program in VC++, but quite often, I find myself writing console utilities for various stuff. VC++ and MFC have some great macros (C++ #define
s) to help you out in debugging. But if you aren't using MFC in your project, then you are stuck. Well, those macros aren't a lot of magic and are quite easy to reproduce for your non-MFC projects (and non VC++ projects too.)
One of the biggest reasons (according to me, at least) there are bugs in the code, is incorrect assumptions about what a particular piece of code is supposed to (and not supposed to) do. So the way out is to document your assumptions in code, and make your code give you warnings when it is not working as expected, or is being used (or misused) in a wrong way.
Some theory (I hope it's not boring) - Bertrand Meyer introduced the concept of design by contract to do exactly the above (and much more, but we don't want to get in those gory details in this article.) But C++ does not support contracts like the way eiffel does. So what do we do. We try to come up with our ways of implementing contracts.
What the hell is this contract I'm talking about? I don't need no legal mumbo jumbo for programming. You don't. Basically, a contract for a function (or method) indicates what conditions it expects to be satisfied to be able to work correctly, what conditions are imposed upon it by the caller, and finally if there are some global conditions which shouldn't be violated.
The conditions which must be satisfied for a function to work correctly are its preconditions or "REQUIRE"-ments. For example, arguments shouldn't be null
is a very common requirement. The conditions imposed by the caller of a particular function must be met. This is to "ENSURE" that result is as expected. For example, if function involves working with files, that the operations were indeed completed, and not aborted. The global conditions are typically called invariants or "ASSERT"-ions.
When we are debugging or in development, we want to be informed when the contracts are broken. Because when the contract is broken, it indicates that the result may not necessarily be correct and something is amiss. One of the easiest ways is to print messages to the console like preconditions not satisfied, assertions failed, or so and so variable has this value while this assertion failed. Well, here are some macros that do exactly that.
#define DBGOUT cout //you could just as easily put
#define REQUIRE(cond) \
if (!(cond))\
{\
DBGOUT << "\nPrecondition \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
#define ENSURE(cond)\
if (!(cond)) \
{\
DBGOUT << "\nPostcondition \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
#define ASSERT(cond) \
if (!(cond)) \
{\
DBGOUT << "\nAssertion \t" << #cond << "\tFAILED\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";\
};\
#define TRACE(data) \
DBGOUT << "\nTrace \t" << #data << " : " << data \
<< "\t\t" << __FILE__ << "(" << __LINE__ << ")";
#define WARN(str) \
DBGOUT << "\nWarning \t" << #str << "\t\t" \
<< __FILE__ << "(" << __LINE__ << ")";
Using the Code
Let's take the example of a function to divide two numbers and see where these macros would help us.
int div (int a, int b)
{
REQUIRE(b != 0)
int result = a/b;
ENSURE(b*result <= a)
return a/b;
}
The above example may look trivial, but consider more complicated functions, and with proper use of the above macros, you will at least the most common causes of errors. TRACE
and WARN
macros will be particularly useful in figuring out why something is not what it should be, and ENSURE
, REQUIRE
and ASSERT
will give you confidence that your functions, if given correct input, will produce correct output.
Slightly more complicated example:
int Parse(char* str)
{
REQUIRE(str != NULL)
int result;
ASSERT (lookupindex < tablesize)
ENSURE(result >= 0)
return result;
}
Misuse
- There are some drawbacks and misuse of these macros. The first is, they slow down processing. So, you typically don't want them in the release code, only while developing or debugging, in that case you conditionally define the macros to produce code in debug, but not in release (see the zip file, it is defined that way in the dbgmacros.h).
- Secondly, the macros expect to test invariant conditions and don't expect some processing to be done in them. For example,
ASSERT(i++!=10)
- this code will most possibly fail in your release. Don't do processing in the macros. Only check conditions.
Points of Interest
To dig further, I urge you to dig deeper into the design by contracts philosophy. It has tremendous impact on how object oriented systems are designed and implemented.
I will come up with similar macros to ease up your testing in the next part of this article, probably next week.
History
- March 10th, 2003: First revision
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.