Table of content
- Write pre/post conditions in one block before the body of your C++
function for a clear reading.
- Have them automatically inoculated in your
doxygen documentation.
- Enable/Disable conditions checks at compile time (if you want to remove
the code for release version).
- Breaks when condition fails or always ignore it at runtime.
- Having hook to handle condition failure.
Design by Contract
"Design by Contract" is a very good method for programming. (Read
Object
Oriented Software Construction from Bertrand Meyer.)
For people who have never heard about DbC, I do a very simple and quick
introduction to the principle.
The idea is that between a function and the call of a function, there is a
contract.
To ensure that the function will do what it should do, every
condition of the contract has to be ok.
Those conditions can involve the function or the
caller.
When a condition
fails, one of the two contractors have not done his job. So the programmer has to choose to change the function or the way the function has
been called.
For example I choose to write a linear interpolation function.
<br>
void lerp( float& _o, float _t, float _s, float _e )
{
_o = _s + ((_e - _s) * _t);
}
We can add some preconditions like :
- _t has to be in [O..1].
- _e has to be a float value.
- _z has to be a float value.
We can add as postcondition :
Imagine the same function but working with vectors or matrix. We should add
conditions saying that the input parameters should not be changed by the
function.
In C++ this condition can be check at compile time (in the case) using the
const keyword.
But for the others conditions we should use the assert function.
void lerp( float& _o, const float _t, const float _s, const float _e )
{
assert((_t >= 0.f)&&(_t <= 1.f));
assert(!isnan(_s));
assert(!isnan(_e));
_o = _s + ((_e - _s) * _t);
assert(!isnan(_o));
}
For more specialized lerp functions you would probably add more sophisticated postconditions like insure othogolalization on matrix ...
Little personal trick : Don't ignore "stupid" conditions, because those are
often the ones which appears. Most of the time it is because the code changes,
late in the night, by copy paste block of code..., and the "stupid" condition
which has been set becomes really relevant.
The Digital Mars C/C++ compiler is
DbC compliant.
DbC with no DbC compliant C++ compilators.
Usually, I write contract condition like this:
int foo( void* p )
{
assert( p );
... code ...
if(...)
{
assert( post condition);
return ...
}
... code ...
if(...)
{
assert( post condition );
assert( an other post condition);
return ...
}
... code ...
... once again check all post conditions ...
return ... ouf
}
So "debug" code, or "check" code really pollute my code, mainly because of
repetitions. And I didn't want to
have those stuff compiled for the the final release.
So I really want to be able to write the function in a better way ..
like:
int foo( void* p )
{
#ifdef contract_check
PRECONDITION( p, "p can't be 0" );
POSTCONDITION( postcondition, "description of my post condition" );
POSTCONDITION( checkpostcondition2(), "an other post condition");
#endif // contract_check
... code ...
if(...)
return ...
... code ...
if(...)
return ...
... code ...
return ...
}
And, because I'm using a super code documentation generator (Doxygen), I
wanted to see the documentation of my assertions (conditions) in the
documentation. Of course, without writing it twice ... Do you follow me?
(redundancy means future problems)
So the little set of macro I wrote gives you the ability to write something
like:
int AClass::Test( int argA, int argB )
{
DBC_BEGIN
DBC_PRE_BEGIN
DBC_PRE( argA >= 0, precond 1 documentation and message )
DBC_PRE( argB != 0, precond 2 documentation and message )
DBC_PRE_END
DBC_POST_BEGIN
DBC_POST( argA + argB >= 0, post condition 1 documentation and message )
DBC_POST( ChechDataFct( argA, argB ), post cond 2 : cf. ChechDataFct() )
DBC_POST_END
DBC_END
... code ...
return ...;
}
And (if you use Doxygen) the documentation will be up to date.
Take a look at this picture taken from Doxygen documentation of the
AClass::Test
function:
While compiling for WIN32, you have this following message when the condition
fails:
Click "OK" to continue (break when using a debugger).
Click "Cancel" to ignore (means that it will not popup a message (or break)
if the condition fails again).
It works with VC6 and VC7.
The code is composed of 2 parts:
- Core stuff: macro definition ...
- Callback stuff: write the code you want to handle assertion.
For example, you can choose to send an exception when a condition fails.
The demo zip file is exactly the same as the source zip file, plus a
compressed HTML doc.
This documentation file can be generated from the source zip file if you have
Doxygen.
Because 100% of the feedback I receive (...2) asks me for explanation about
the trick I have used to make the tool working, I added this section to the
article.
Doxygen documentation:
The Doxygen documentation works using the Doxygen preprocessor :
In source file, the line:
DBC_PRE( argA >= 0, precond 1 )
Using in the Doxygen preprocessor:
DBC_PRE(a,b) =\pre b \code a \endcode
the code becomes for Doxygen:
\pre precond 1 \code argA >= 0 \endcode
which is a valid command for Doxygen parser.
C++ code:
Note: I assume you know what are functions prolog
and
epilog
, how the compilers use the stack in the calling
function process.
The main problem is ... post condition of course. You see easily that calling
the preconditions at the begin of a function is very easy ... because you have
written it at the beginning :)
But I use a little trick to call the post conditions after returning the
function.
The process is very simple: Meanwhile the epilog of the function (so when the
function ends/returns/leaves), we go to the start of the postcondition, execute
postcondition block, and finish the epilog.
Before continuing to explain the trick, you should see that there is another
problem: do not call POSTCONDITION block before the end of the function.
As it is written just before the body of the function, we have to skip it.
The algorithm to execute the function becomes:
- execute precondition block
- jump to body
when return :
- goto postcondition block
- execute postcondition block
- return back to epilog of the function.
So the first step is to tag the poscondition block. It is very easy because
it is enclosed by 2 macros: DBC_POST_BEGIN
and
DBC_POST_END
.
We also need to know where the start of the body of the function is. The
answer is ... after the "Design by Contract" block ... enclosed by
DBC_BEGIN
and DBC_END
.
The pseudo macros become something like :
define DBC_BEGIN
define DBC_END __dbc_body :
define DBC_PRE_BEGIN
define DBC_PRE_END
define DBC_POST_BEGIN if(not in epilog) __asm je __dbc_body
define DBC_POST_END continue epilog (means jump after
caller instruction in epilog ...)
Using C++ local variable declaration:
I often use C++ local variable declaration, or even local static variable
declaration to do many things. I've plan to write something about that in
another article because it is a powerful feature of C++, and I never read
anything about taking benefits of that.
Here, I just use the fact that when we declare a local instance of a class,
the destructor will be called during the epilog of the function. So declaring an
instance of a class with destructor gives you an "epilog trap", which is the
destructor of the class.
I also use the fact that constructors are called where the variable is
declared... but it is not very relevant for this tool. I mean, it can be
implemented differently I suppose.
Here is the code of the class declaration used by this tool:
class structDBC
{
public :
structDBC();
~structDBC();
long dbc_postblockRETaddr;
long dbc_postblockaddr;
};
Add our class in the macros:
define DBC_POST_BEGIN structDBC autoDBC;
if(!autoDBC.dbc_postblockRETaddr) goto __dbc_body;
define DBC_POST_END goto autoDBC.dbc_postblockRETaddr;
So post condition block is enclosed by:
- IF
dbc_postblockRETaddr
is not set (we are not called by
epilog) we goto the start of the body. ELSE (called by epilog) we execute the
post condition block.
- AT the end of the postcondition block, we jump (goto) the next instruction
in the epilog (which is stored in
dbc_postblockaddr
).
The structDBC constructor and destructor implementation:
Note:
- Constructor and destructor are implemented in a separate file, to ensure
that they will be called with a
call
instruction (no
jmp
). According to compilers, it is possible to add some
special declaration like __noinline
to insure that. I think that
usually compilers will implement call of those functions with call
because they are implemented in a separate object.
__declspec(naked)
indicates Visual C compiler to not create
epilog et prolog to this function.
__declspec(naked) structDBC::structDBC()
{
__asm push ebp
__asm mov ebp, esp
__asm mov eax, ebp
__asm add eax, 4
__asm mov eax, [eax]
__asm mov dword ptr [ecx+4], eax
__asm mov esp, ebp
__asm pop ebp
__asm mov dword ptr [ecx], 0
__asm ret
}
__declspec(naked) structDBC::~structDBC()
{
__asm push ebp;
__asm mov ebp, esp;
__asm pusha;
__asm mov eax, ebp;
__asm add eax, 4;
__asm mov eax, [eax];
__asm mov [ecx], eax;
__asm popa;
__asm mov esp, ebp;
__asm pop ebp;
__asm add esp, 4;
__asm jmp dword ptr [ecx+4];
}
Note: if you compare with the code of the source, you will notice that
I don't use a class, but a struct
... This is just because I want
to ensure that no virtual table will be implemented (or other stuff the C++
compiler wanted to add)... according to assembler code. Writing this explanation
paragraph, I notice that I never use the attributes names of the class... It is
just because ... I don't need it while coding in assembly.
Final Macros :
#define DBC_POST_BEGIN structDBC autoDBC; \
__asm cmp dword ptr [autoDBC], 0 \
__asm je __dbc_body
#define DBC_POST_END __asm jmp dword ptr [autoDBC];
It is the first article I publish and ... sorry for my English.
I hope it will be useful to others.
Unfortunately, I haven't tested it very much, just on a few targets:
the code depends on compiler.
But, the idea is here, everybody can adapt and improve the stuff (please
share your add-on or improvements :) )
I tried to do those stuff using only C/C++, for example using
longjump
. But I didn't find a way to get what I wanted.
If somebody can do it without assembly: please send me your code :)
- April 2004 : Add code documentation after
Kandjar comment.
- October 2004 : Add DbC introduction after Peterchen comment.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.