A simple scheme to implement Design by Contract in C++
Luca -
☕ 5 min. read
Recently I got interested in C++ again. The new lambda functions in C++ 11 open up a world of opportunities for C++ programmers. I’ll talk more about how you can write functional code in C++ 11 in upcoming posts. For now let’s look at design by contract.
Design by contract is a development style promoted by Bertrand Meyer and it is implemented in his own Eiffel programming language. At core, it advocates using preconditions, postconditions and invariants.
An invariant is an assertion that always holds true for a class after the class has been fully constructed and if the code is not executing inside a method. As a user of the class, you always observe the invariant to be true. As an implementer of the class, you can be assured that the invariant is true before a method is entered and you need to make the invariant true again by the time your method exits.
A preconditions is an assertion that needs to hold true at the start of a function, for the postcondition to be true at the end of it. Taken together, invariant, precondition and postcondition define the contract between the implementer and the user of a class.
Code for this post is here and here. Thanks to Andy Sawyer, Steve Bower and Ganesh Sittampalam for reviewing my code and suggesting improvements.
Preconditions are simple and everyone uses them. They are those little if statements that you put at the start of your functions to make sure that the caller has given you the right parameters.
double divide(double x, double y) { if(y == 0) throw new exception(“y cannot be 0”);
… }
These little ‘if’ statements don’t really make the precondition stand out. They can be confused with other, unrelated, ‘if’ statements that do completely different semantic things. A more readable alternative is:
double divide(double x, double y) { requires(y != 0); …
}
Not an impressive difference, for sure, but kind of nice. The evil macro looks like this:
#ifndef ___PRECOND #define requires(F) {if((!(F))) throw preexception(__FILE__, __LINE__,"Pre-condition failure: " #F);}; #else #define requires(F) #endif
Note that the exception maintains information not just about the file and line number of the failure, but also a textual representation of the failed condition. Such things you can do with macro magick.
Postconditions are trickier. In the case of a side-effect free (pure) function, a postcondition asserts something of interest about the return value. In the case of a class, it asserts something of interest about the state of the class before and after the execution of the method.
Let’s start with a pure function. I like to have all my assertion at the start of the function to allow reasoning about it without looking at implementation details. But that poses the problem that the result is available just at the end of the function. My solution is to enforce this idiom:
double divide(double x, double y) { double result; requires(y != 0); ensures(result < x); // Silly, just to falsify it in tests
… return result; }
So you need to declare your result upfront. That is the biggest limitation of the overall solution in my opinion. If that is acceptable to you, the trick now is how to execute the postcondition test before the method exits. We can do that by storing a lambda and executing it in the destructor:
typedef std::function<bool ()> ___dbcLambda; class ___post { public: ___post(const char *file, long line, const char *expr, const ___dbcLambda& postF) : _f(postF), _file(file), _line(line), _expr(expr) {} ~___post() { if( !std::uncaught_exception() && !_f() ) { throw postexception(_file,_line,_expr); } } private: const ___dbcLambda _f; const char * const _file; const long _line; const char * const _expr; };
You might think that you shouldn’t throw exceptions in a destructor. That is something I never understood about the RAII pattern in C++. If I choose to use exceptions as my error notification method, how am I supposed to get notified if there is a problem releasing a resource in RAII, other than by throwing an exception in the destructor?
Maybe because of this, the standard has an uncaught_exception() function that allows you to check if an exception has been thrown, so that you don’t throw another one during stack unwinding. If you really don’t like throwing in the destructor, feel free to assert.
You might be worried about performance, but you really shouldn’t as you can disable all these macros in Release.
The macro then creates a ___post class on the stack.
#define ensures(F) \ int ___UNIQUE_LINE = __LINE__; \ auto ___UNIQUE_POST = ___post( __FILE__, __LINE__, "Post-condition failure:" #F, [&](){return (F);});
The UNIQUE stuff is messy business. Part of it is by design and it is used to make sure that each __post variable has a unique name to have multiple ‘ensures’ in a function. The other part is a workaround for this msvc bug. Let me know if you want more details. I suspect there is a better way to do it.
Here is the full enchilada …
#define ___MERGE(a, b) a##b #define ___POST(a) ___MERGE(___postcond,a) #define ___UNIQUE_POST ___POST(__LINE__) #define ___LINE(a) ___MERGE(___line, a) #define ___UNIQUE_LINE ___LINE(__LINE__)
The case in which a postcondition is used inside a method of a class is even trickier because the postcondition must be able to compare the state of the class at the entrance of the method to the state of the class at its exit. Assuming a Counter object with an Add method and assuming ‘___pre’ captures the state of the counter at the start of the method, you’d like to write something like:
void Add(int x) { ensuresClass(this->c_ == ___pre.c_ + x);
… }
Now, this is tricky. The only way to capture the ‘old’ state in ‘___pre’ is by making a copy of it and store it there. This is what the code below does:
#define ensuresClass(F) \ auto ___pre(*this); \ auto ___UNIQUE_POST = ___post( __FILE__, __LINE__, "Post-condition failure: " #F, [&](){return (F);});
More troubling is the possibility that the class doesn’t have a copy constructor. In that case you explicitly need to associate a value with ‘___pre2’ by passing it as the first parameter to the appropriate macro as in the code below:
void Add(int x) { ensuresClass2(this->c_, c_ == ___pre2 + x); }
Which is implemented as follows:
#define ensuresClass2(ASS,F) \ auto ___pre2(ASS); \ auto ___UNIQUE_POST = ___post( __FILE__, __LINE__, "Post-condition failure: " #ASS " is ___pre2 in " #F, [&](){return (F);});
And I know about the giant ass …
Now for invariants. The user should implement an isValid() method on his class as below:
bool isValid() { return c_ >= 0;}
Then he should add an ‘invariant()’ call at the start of each method, at the end of each constructor and at the start of each destructor:
void Add(int x) { invariant(); requires(x < 10); ensures(this->c_ == ___pre.c_ + x);
… }
This calls the ‘isValid’ function at the start of the method and at the end of it using the same destructor trick:
#define invariant() \ if(!(this->isValid())) throw preexception(__FILE__, __LINE__,"Invariant failure"); \ auto ___UNIQUE_INV = ___post( __FILE__, __LINE__, "Invariant failure", [&](){return this->isValid();});
All the above machinery is not at all equivalent to having such constructs in the language, but it is simple enough and with a decent enough syntax to be interesting.
Now a caveat: I have no idea if any of this works. It does work in my examples and its behaviour seems reasonably safe to me, but I haven’t tried it out on any big codebase and haven’t stressed it enough for me to be confident recommending its usage. So, use it at your own risk, let me know how it goes.
0 Webmentions
These are webmentions via the IndieWeb and webmention.io.
11 Comments
Comments
bentsch
2012-03-10T18:00:51ZGenerally, I like the idea.
But you might want to consider what Herb Sutter has to say about this: http://www.gotw.ca/gotw/047...
Ben
lucabol
2012-03-11T10:38:45ZThanks for the link. It is hard to disagree with Herb, but I'll try :-)
He doesn't like using uncaught_exception in a destructor on practical and moral grounds.
1. On practical grounds, doing so is not composable in certain scenarios. Meaning that using RIIA objects that rely on it in destructors needs to be done carefully. That is true, but that doesn't disqualify the solution. Many things are useful, but need to be used carefully. In the case exposed in this blog, that is not a problem. The behavior would be correct.
2. On moral grounds, you often need to report errors differently if you are already in an error condition. I.E. you don't want to send e-mail to your user twice, you don't want to report the second (bogus) error in a parser, you don't want to allocate memory if you got an out-of-mem exception etc ...
More generally, I fail to see a better way to manage errors in releasing resources in RIIA. All the ones I've seen (i.e. logging to file) are gross hacks that don't compose at all.
But if you still are un-convinced by my argument, feel free to change the throwing code with an assert and things work pretty much the same.
bentsch
2012-03-11T23:37:11ZYes, I would definitely use an assertion, bypassing this whole problem.
But I wonder why you use exceptions at all? From my understanding (which might be wrong, as I have never read any books about dbc) a violated contract indicates a programmer's error (a bug), not an exceptional situation. Thus, if your post-condition is violated your data is very likely to be in an inconsistent state. How would you even react to such a dbc-exception being caught?
Ben.
lucabol
2012-03-12T09:05:49ZIf you leave them on in Release mode (which seems to be more frequently done for precondition), then you want to treat them as any other error (i.e. clean up resources, logging and reporting to user).
In Debug, it probably doesn't makes a difference. I believe that's true of exceptions in general.
bentsch
2012-03-12T19:06:08ZYou should distinguish carefully between runtime errors that lie outside of your program (occurring most often when acquiring or accessing a resource, e.g. a file can not be opened) and bugs (incorrect code, in this case contract violations).
The first case can and should be treated via exceptions. These can be handled at run-time. The latter can not be safely dealt with via exception handling, as your program is already in an inconsistent state. If unlucky, you might totally crash your program when throwing an exception as your destructors' preconditions might be invalid. If even unluckier, you might end up with a program whose data only gives the impression of being consistent.
My take on violated contracts in release mode would be to implement a functionality that is orthogonal to exception handling by providing an appropriate callback function. This callback function would then inform the user, log the error, try to write out any data that could help the user restore is work later on (which might already fail badly), possibly clean up and finally... call std::terminate().
Ben.
lucabol
2012-03-12T20:09:15ZIt is not possible to draw the line so sharply. What is a resource error for one user of your library, might be a coding error for another. That's why most programming environent (i.e. .NET, Java, PHP, ...) have an invalid argument exception that gets thrown when a precondition is violated.
You distinguish between the two cases by catching the different kinds of exception and doing something different about them. The catching is done outside of your library, which is the only place where you have enough information to really know what to do. Who knows, for the space shuttle, calling terminate() for a bug might not be the right action. You don't know what to do, the user of your library does.
bentsch
2012-03-13T22:58:47ZI see your point.
Although I would try to treat the two error sources differently in production code, I agree that in library code it might not always be possible to make a clear distinction.
Thanks for the feedback.
Ben.
bentsch
2012-03-13T23:10:37Z.... though having thought about it for a second I would tend to go for the callback approach instead of the exception path in library code: After all, you can still implement a callback handler which throws an exception. But it's not possible the other way around.
lucabol
2012-03-14T00:01:59ZFrankly, I don't see the advantages of a callback approach compared to an exception approach. In both cases you let the user decide which code runs when an error occurs. Both approaches let's you differentiate between different categories of errors. The callback approach has the drawbacks of being significantly more cumbersome to use.
Most 'modern' libraries out there (.net, java, etc...) go for the exception approach. Most of the old style ones go for a 'returning an error code' approach. There are advantages and disadvantages in both approaches (topic for another post), but I don't see the need for a third one.
Good (?) thing about C++ is that it allows us vast latitude on how we do things ...
bentsch
2012-03-14T20:46:47Z"In both cases you let the user decide which code runs when an error occurs. Both approaches let's you differentiate between different categories of errors."
That is exactly not the case. You have no idea what destructors will be called.
"...I don't see the need for a third one."
It's actually not a new approach. Think of is as enabling assertions for your release build (maybe the term callback was misleading). Also, when operator new() fails a similar approach is taken in calling an error handler, which can be changed using std::set_new_handler().
From my point of view exceptions signal error conditions that might be recoverable. Now while we can discuss about pre-conditions this is clearly not the case when your class invariant is broken or your post-condition is invalid. And then you're trying to fix bugs at runtime.
Ben.
lucabol
2012-03-15T14:39:04ZHi Ben,
Le'ts just agree to disagree here. I think we are running in circles.
In the end, it is trivial to plug whatever error reporting mechanism you feel is best in this framework.
Cheers,
.luca