Journal Articles
Browse in : |
All
> Journals
> CVu
> 293
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Living Within Constraints
Author: Bob Schmidt
Date: 07 July 2017 18:04:54 +01:00 or Fri, 07 July 2017 18:04:54 +01:00
Summary: Pete Goodliffe constrains what’s possible in your code.
Body:
We have already considered defensive programming techniques (in CVu 29.1) to help make your code better. These techniques force you to consider what might go wrong – to assume the worst. So how can we physically incorporate these assumptions into our software so they’re not elusive problems waiting to emerge? Clearly, we can simply write a little extra code to check for each condition. In doing so, we’re codifying the constraints on program functionality and behaviour.
Constraints
The most obvious implementation of constraint checking in C and C++ is the humble assert
. We’ll look at it in detail later, but it’s simple to use:
assert(itemIndex < maxNumItems);
When running a program with assertions enabled (usually this has to be within a ‘debug’ configuration build) a failure to satisfy the assertion will force the program to unceremoniously stop. Immediately. The program will dump out some diagnostic information about the assertion failure. But it will do so in an ugly way the programmer will understand. It’s definitely not a nice user experience!
assert
is simple, but can sometimes be a sledgehammer used to crack a walnut. There’s no subtly; you cannot control what it does (apart from switch it on or off).
In a mature system, what do we want the program to do if a constraint is broken?
Since this kind of constraint will likely be more than a simple detectable and correctable run-time error, it must be a flaw in the program logic. There are few possibilities for the program’s reaction:
- Turn a blind eye to the problem, and hope that nothing will go wrong as a consequence.
- Give it an on-the-spot fine and allow the program to continue (e.g., print a diagnostic warning or log the error).
- Go directly to jail; do not pass go (e.g., abort the program immediately, in a controlled or uncontrolled manner – either allowing it to neatly clean up if possible, or just exiting like assert).
Indeed, you may have different types of constraint condition that require different ways of handing.
For example, it is invalid to call C’s strlen
function with a string pointer set to zero, because the pointer will be immediately dereferenced, so the latter two options are the most plausible. It’s probably most appropriate to abort the program immediately, since dereferencing a null pointer can lead to all sorts of catastrophes on unprotected operating systems.
Checking a value is ‘sane’ before displaying it on the screen might not need such a brute force approach.
Many debug/logging libraries provide constraint checking services. Generally they are configurable and flexible. Constraint checking is often bound in logging libraries because often you want to log a broken constraint, but allow the application to continue nevertheless.
How to use constraints
There are a number of different scenarios in which constraints are used:
- Preconditions These are conditions that must hold true before a section of code is entered. If a precondition fails, it’s due to a fault in the client code.
- Postconditions These must hold true after a code block is left. If a postcondition fails, it’s due to a fault in the supplier code.
- Invariants These are conditions that hold true every time the program’s execution reaches a particular point: between loop passes, across method calls, and so on. Failure of an invariant implies a fault in the program logic.
- Assertions Any other statement about a program’s state at a given point in time.
The first two are frustrating to implement without language support – if a function has multiple exit points, then inserting a postcondition gets messy. Eiffel supports pre- and postconditions in the core language and can also ensure that constraint checks don’t have any side effects.
Good constraints expressed in code make your program clearer and more maintainable. This technique is also known as design by contract, since constraints form an immutable contract between sections of code.
Without built-in language-level constraint checking, the code mechanism used to check each of type of constraint is usually identical, and the type of constraint is just implicit from its location in the code.
What to constrain
There are a number of different problems you can guard against with constraints. For example, you can:
- Check all array accesses are within bounds.
- Assert that pointers are not zero before dereferencing them.
- Ensure that function parameters are valid.
- Sanity check function results before returning them.
- Prove that an object’s state is consistent before operating on it.
- Guard any place in the code where you’d write the comment We should never get here.
The first two of these examples are particularly C/C++ focused. Other languages have their own ways of avoiding some of these pitfalls.
Just how much constraint checking should you do? Placing a check on every other line is a bit extreme. As with many things, the correct balance becomes clear as the programmer gets more mature. Is it better to have too much or too little? It is possible for too many constraint checks to obscure the code’s logic. Readability is the best single criterion of program quality: If a program is easy to read, it is probably a good program; if it is hard to read, it probably isn’t good.
Realistically, putting pre- and postconditions in major functions plus invariants in the key loops is sufficient.
Removing constraints
This kind of constraint checking is usually only required during the development and debugging stages of program construction. Once we have used the constraints to convince ourselves (rightly or wrongly) that the program logic is correct, we would ideally remove them so as not to incur an unnecessary runtime overhead.
Thanks to the wonders of modern technology, all of this is perfectly possible. As we’ve already seen, the C and C++ standard libraries provide a common mechanism to implement constraints – assert
. assert
acts as a procedural firewall, testing the logic of its argument. It is provided as an alarm for the developer to show incorrect program behaviour and should not be allowed to trigger in customer-facing code. If the assertion’s constraint is satisfied execution continues. Otherwise, the program aborts, producing an error message looking something like this:
bugged.cpp:10: int main(): Assertion "1 == 0" failed.
assert
is implemented as a preprocessor macro, which means it sits more naturally in C than in C++. There are a number of more C++-sympathetic assertion libraries available.
To use assert
you must #include <assert.h>
. You can then write something like assert(ptr != 0);
in your function. Preprocessor magic allows us to strip out assertions in a production build by specifying the NDEBUG
flag to the compiler. All assert
s will be removed, and their arguments will not be evaluated. This means that in production builds assert
s have no overhead at all.
Whether or not assertions should be completely removed, as opposed to just being made non fatal, is a debatable issue. There is a school of thought that says after you remove them, you are testing a completely different piece of code. Others say that the overhead of assertions is not acceptable in a release build, so they must be eliminated. (But how often do people profile execution to prove this?)
Either way, our assertions must not have any side effects. What would happen, for example, if you mistakenly wrote:
int i = pullNumberFromThinAir(); assert(i = 6); // hmm - should type more // carefully! printf("i is %d\n", i);
The assertion will never trigger in a debug build; its value is 6 (near enough true for C). However, in a release build, the assert
line will be removed completely and the printf
will produce different output. This can be the cause of subtle problems late in product development. It’s quite hard to guard against bugs in the bug-checking code!
It’s not difficult to envision situations where assertions might have more subtle side effects. For example, if you assert(invariants());
, yet the invariants()
function has a side effect, it’s not easy to spot.
Since assertions can be removed in production code, it is vital that only constraint testing is done with assert
. Real error condition testing, like memory allocation failure or file system problems, should be dealt with in ordinary code. You wouldn’t want to compile that out of your program! Justifiable run-time errors (no matter how undesirable) should be detected with defensive code that can never be removed.
Java has a similar assert
mechanism, which throws an exception (java.lang.AssertionError
) instead of causing a program abort. .NET provides an assertion mechanism in the framework’s Debug
class.
When you discover and fix a fault, it is good practice to slip in an assertion where the fault was fixed. Then you can ensure that you won’t be bitten twice. If nothing else, this would act as a warning sign to people maintaining the code in the future.
A common C++/Java technique for writing class constraints is to add a single member function called bool invariant()
to each class. (Naturally this function should have no side effects.) Now an assert
can be put at the beginning and end of each member function calling this invariant. (There should be no assertion at the beginning of a constructor or at the end of the destructor, for obvious reasons.) For example, a circle
class invariant may check that radius != 0
; that would be invalid object state and could cause later calculations to fail (perhaps with a divide by zero error).
Questions
- How much constraint checking do you employ in your codebase?
- Are there some functions that benefit more from pre/post condition checking? Why?
- How can you ensure that the logic in a constraint expression will have no observable affect on the program’s behaviour?
Notes:
More fields may be available via dynamicdata ..