Introduction
When declaring an interface (as a class for example), there is a need to define the allowed calling order of functions. For example, Init()
must be called before any other function, and nothing should be called after Shutdown()
.
In the spirit of design by contract, I wrote this tiny class + macros to make such rules enforceable in runtime.
See also:
This code was tested on VC 6 and should work on any C++ compiler.
Background
Design By Contract (DBC) is an issue that many people talk about but I did not see much practical work in the environment I work with (C++). DBC is all about clearly defining what your code should do (This might not be the best definition, but it will do for now).
While working on a complex project I found a bug caused by forgetting to call Init()
before the function I needed. Of course, it happened only in certain control flows, to make it more challenging.
The solution: Check in runtime that things happen as you expect them to be. Putting ASSERT()
s helps but it is not enough by itself.
Using the code
The code consists of one class and a few macros to make the source readable. For example, the statement function B::foo() should be called after Init() and not after shutdown()
is written as:
void B::foo()
{
Enter("foo");
Require(Called("Init"));
Require(!Called("shutdown"));
}
The full list of macros you can use (see also the .h file):
Enter(name)
- call this as the first thing in your function, unless this is your Init()
.
Called(name)
- return true
if function name
was already called (anytime in the past).
FirstCall
- return true
if no functions were called, including this function.
Prev(name)
return true
if name
was called just before the current function.
Require(boolean expression)
will blow a fuse if expression evaluates to false
. Currently it is simply ASSERT()
.
To use the code, include DBC_EnforceOrder.h and in the class declaration add a macro:
class B
{
private:
DBC_Data;
};
In each function that may influence the state of "what is allowed to be called next", you have to add the Enter(name)
macro as in the B::foo()
.
Finally, to turn it off, make sure the macro _EnforceCallOrder
is undefined. Currently it is defined in the H file.
Drawbacks
- In every function (of interest) you must add
' Enter("function_name")
. In another article I wrote, there is a way around it using symbol table engine - but this complicates the code enormously.
- I could add special rules such as
LAST_FUNCTION
, and then check for this status in the Enter()
call. I chose not to do it for the tradeoff between simplicity and elegance.
Future additions
- So far, only
REQUIRE
(i.e. precondition) is implemented. It would be nice to add ENSURE
for post condition checking as well.
- call
STACK_TRACE()
from Enter()
to have the full monti.
Points of interest
I wrote this code after failing to find in the Internet, the exact solution to what I needed. There are plenty of articles on DBC, but not so much frameworks to work with. And remember the KISS principle!
History
- 2003-09-25: First release