Design by Contract macros for C++ and link to Doxygen






4.04/5 (34 votes)
Macros to write "Design by Contract" conditions in the header of the functions and inoculate it in your doxygen documentation automatically.
Table of content
Purpose of the provided macros
- 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.
Introduction
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.
/**@brief Float linear interpolation.
* @param _o lerp result.
* @param _t parametric value.
* @param _s start value.
* @param _e end value.
**/
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 :
- _o has to be a float.
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 ); // precondition : p cannot be null. ... code ... if(...) { assert( post condition); // post condition documentation return ... } ... code ... if(...) { assert( post condition ); // doc // this condition didn't have to be check in the last return assert( an other post condition); // documentation 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" ); // the phrase is to display to the user // why the assertion fail. 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.
Example:
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).
Code
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.
How it works?
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 // declare start of body label. 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(); // constructor ~structDBC(); // destructor // store caller address of post condition block // (means instruction address which call destructor in epilog). long dbc_postblockRETaddr; // store start address of postcondition block. 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 (nojmp
). 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 withcall
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()
{
// standard prolog (__asm ENTER)
__asm push ebp;
__asm mov ebp, esp;
// Put in eax the stack pointer where return address is store
__asm mov eax, ebp;
__asm add eax, 4; // because we have done a push on the first instruction.
// store address effective return adress in dbc_postblockaddr
// made in 2 lines because we can't write
// __asm mov dword ptr [ecx+4], [eax]
__asm mov eax, [eax]; // store effective address in eax.
__asm mov dword ptr [ecx+4], eax; // copy to dbc_postblockaddr
// standard epilog (__asm LEAVE )
__asm mov esp, ebp;
__asm pop ebp;
__asm mov dword ptr [ecx], 0 // reset dbc_postblockRETaddr
__asm ret;
}
__declspec(naked) structDBC::~structDBC()
{ // standard prolog __asm push ebp; __asm mov ebp, esp; // put return address in dbc_postblockRETaddr __asm pusha; __asm mov eax, ebp; __asm add eax, 4; __asm mov eax, [eax]; __asm mov [ecx], eax; // copy to dbc_postblockRETaddr __asm popa; // standard epilog __asm mov esp, ebp; __asm pop ebp; __asm add esp, 4; // skip next line. // goto dbc_postblockaddr (start of post condition block). __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];
Conclusion
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 :)
Visit
- Building bug-free O-O software: An introduction to Design by ContractTM
- Doxygen and 10 Minutes to document your code
- Digital Mars C and C++ compiler
History
- April 2004 : Add code documentation after Kandjar comment.
- October 2004 : Add DbC introduction after Peterchen comment.