Design by Contract Programming in C++
The Eiffel programming language introduced "design by contract" to object oriented programming. The main idea here is to model interfaces between classes as contracts. In this article, we will be applying this powerful technique to C++ programming.
Interfaces as Contracts
In legal terms, a contract is a binding document that describes the responsibilities and expectations of the parties entering into the contract. Interfaces between classes can be modeled in the same way. Whenever an object invokes a method of an other object, this interaction should be viewed as a contract between the caller and the called method. This contract consists of the following conditions:
- Pre-conditions. The caller of the method needs to pass parameters that meet the requirements of the called method. Pre-conditions check if the caller of the method is keeping its side of the contract by passing the right parameters.
- Post-conditions. The called method needs to return results that meet the expectations of the caller. Post-conditions check if the called method is keeping its side of the contract, i.e. returning the results required by this interface.
- Consistency checks. In addition to the pre-conditions and post-conditions, all the objects involved in the transaction should be left in a consistent state.
Design by Contract Framework
We will consider an example here to see how "design by contract" is implemented in C++. We start by looking at the basic framework for design by contract. The framework consists of the following macros that are available only in the debug mode. The macros are defined to blank in the release build.
- ASSERT: This macro aborts the program if the parameter passed to the macro does not evaluate to true. When this macro is used in the code, the programmer is asserting that the condition specified in the parameter has to be true. If it is not so, further execution of the program cannot proceed.
- IS_VALID: This macro is used to check the consistency of an object. The programmer invokes this method before acting on an object pointer. This method invokes the virtual function IsValid to perform the consistency checks associated with the class.
- REQUIRE: This macro should be used to check if the pre-conditions for the invoked method have been met. If the caller does not keep its part of the contract, this macro will assert (as it is just another redefinition of ASSERT)
- ENSURE: This macro checks if the post conditions have been met. Here the called function checks if it has kept its part of the contract. This macro will assert if there is a contract breach.
- IsValid Method: This method is defined in a base class that will act as the parent for all inheriting classes. This method is defined as an abstract function. This forces all programmers to define the function. The code in the inheriting classes should check for consistency of the objects internal state. Also note that this method exists only in the debug build, thus it does not add to code bloat.
Debug and Release Builds
The "design by contract" framework can work only if the programmers can introduce aggressive checks without having to worrying about their performance implications. The main idea here is that lab testing of the product should be done with debug builds (i.e. _DEBUG flag is defined) where all the "design by contract" macros are enabled. These macros will allow you to zero in on the faults very quickly as all breach of contract conditions are being checked. This can dramatically lower the debugging time in a large project. This means you will have less of those unhealthy finger pointing sessions when bugs have to be isolated.
When the product is ready to be shipped, the release build can be made. This build will disable all the macros used in the "design by contract" framework. Thus you will obtain complete performance. If CPU performance is not a big issue, in the initial deployment you may decide to retain the macros and replace the exit condition in the ASSERT macro with exception throwing. Thus you have complete control on the level of debugging you wish to have in the final product.
An important thing to note here is that the "design by contract" framework is not a replacement for defensive programming. You still write defensive code which handles error conditions even in the release build. You can view the contract checking as a diagnostic programming technique that allows you to be extra suspicious in handling contracts when the implementation of the contract is new and untested. When you get comfortable with the contract implementation, you turn off the extra suspicious checking.
Another benefit of "design by contract" technique is that it gives you extra diagnostic capability without compromising readability. There is no need to add redundant if statements for diagnostic programming. In fact, these macros will actually improve the readability as they clearly state the expectations of the caller and the called methods.