Design by Contract is a software paradigm that, when used, ensures that your code will have fewer bugs. It does so by enforcing "contracts." A contract is made by a function based on what it expects when you enter it (preconditions) and what it yields when it exits (postconditions). For example, an int factorial(int n) function expects n to be positive (precondition), and it will yield a positive value (postcondition). Use the Design by Contract paradigm, and your code will contain fewer bugs—a lot of programmers believe it, and their experience proves it.
How it works
A function employs Design by Contract (DbyC) if it has preconditions and postconditions—conditions that should always be met—and it tests that those conditions indeed exist.
You can employ DbyC in C++ by:
- Using the assert function (or some similar macro)
- Throwing an exception in case a precondition or postcondition fails
- Using assert and also throwing an exception
Using the assert function and throwing an exception is the preferred method. In this article, we'll discuss why it's preferred and how to implement it effectively.
Usefulness of designing by contract
DbyC helps you catch bugs early in two places (Listing A):
- In your DbyC function, if results are wrong for some arguments
- In client code, when you call a DbyC function and you mistakenly pass an incorrect combination of arguments
You should use preconditions when your function does not handle some input data; for instance, when the data wouldn't make sense for that function (e.g., calling a factorial function with a negative value). You should use postconditions to test the correctness of the function output (e.g., the factorial function should return a positive value).
Saying that a contract is broken (it failed a precondition or postcondition) is equivalent to declaring a programming error. It means that either the function or the client code calling the function contains a semantic error. Usually, when either condition fails, the code that is supposed to be implemented after that failed condition should not be executed because the program will most likely crash (Listing B). In light of that possibility, when a DbyC precondition or postcondition fails, you'll want to:
- Find out ASAP that the contract has been broken. This is usually accomplished by breaking into the offending line of code with a debugger.
- Make sure that code following the broken contract is not executed (since the program could crash or produce invalid results). This is usually accomplished by throwing an exception.
If you're developing an application, you'll usually prefer to use the debugger method, because when a bug appears, you'll want to know about it right away. This will be very helpful, especially when connecting different modules from a big application (integration testing).
When you're developing a library, you never know where it will be used. For instance, if someone using your library is building a server application, the debugger option is not viable; therefore, your best bet is throwing an exception.
The biggest problem with the first option is that your program could crash at the customer site. Many experts recommend the second option over the debugger option for this issue alone. I tend to agree more and more with these experts—throwing an exception will make your program safer (Listing C).
Best of both worlds
The bad thing about throwing an exception, however, is that when a contract is broken, you'll find out too late and lose the context in which the problem occurred (Listing D).
Remember that contract is broken is equivalent to programming error. So, before throwing an exception, you can do something extra to retain the context of the problem:
- In Debug mode, break into the debugger when a contract is broken. You won't lose any context, and you'll be able to fix the problem very easily.
- In Release mode, find out where a broken contract occurred and log it somewhere. The program will then continue, and there will be no crash.
Achieving the above is simple: In your code, instead of using throw some_exc(args), just use dbyc_throw some_exc(args). The code for the dbyc_throw macro is shown in Listing E.
When you want to implement custom handling before an exception is thrown, you just define the THROW_CUSTOM_HANDLER macro prior to including <enhance_throw.h>. Then, in a source file, provide the before_throw_exception function.
Listing F shows you how to use the dbyc_throw method.
As you've seen, the code in <enhance_throw.h> is quite small and simplistic, but it does handle most cases. However, if it's not enough for you, the SMART_ASSERT library offers you more options and focuses on providing you with as much information as possible when a contract is broken. In addition, you can specify multiple assertion levels and set how each should be handled. Finally, you can use SMART_ASSERT in Release mode as well.
DbyC is a very powerful paradigm, and there are many ways to implement it in C++. But you should do it wisely. As soon as a contract is broken, make sure you know about it firsthand and use <enhance_throw.h> or the SMART_ASSERT library, or implement your own library based on what you've seen in this article.