• Tidak ada hasil yang ditemukan

Errors!

4.7 Avoiding and finding errors

4.7.3 Assertions

simple problems and simple programs, you can just temporarily put in a few extra output statements (using the error-reporting output stream cerr) to help you see what’s going on. For example:

Click here to view code image

int my_fct(int a, double d) {

cerr << "my_fct(" << a << "," << d << ")\n";

int res = 0;

// ... misbehaving code here ...

cerr << "my_fct() returns " << res << '\n';

return res;

}

Insert statements that check invariants (that is, conditions that should always hold; see §4.7.3 and §8.4) in sections of code suspected of harboring bugs.

For example:

Click here to view code image

int my_complicated_function(int a, int b, int c) // the arguments are positive and a < b < c {

if (!(0<a && a<b && b<c)) // ! means "not" and && means "and"

error("bad arguments for mcf");

// ...

}

If that doesn’t have any effect, insert invariant checks in sections of code not suspected of harboring bugs; if you can’t find a bug, you are almost certainly looking in the wrong place.

programs people work on; others seem to have to do with differences in the ways people think. To the best of our knowledge, there is no one best way to debug. One thing should always be remembered, though: messy code can easily harbor bugs. By keeping your code as simple, logical, and well formatted as possible, you decrease your debug time.

4.7.3.1 Preconditions CC

Now, let us return to the question of how to deal with bad arguments to a function. The call of a function is basically the best point to think about correct code and to catch errors: this is where a logically separate

computation starts (and ends on the return). Look at what we did in the piece of advice above:

Click here to view code image

int my_complicated_function(int a, int b, int c) // the arguments are positive and a < b < c {

if (!(0<a && a<b && b<c)) // ! means "not" and && means "and"

error("bad arguments for mcf");

// ...

}

First, we stated (in a comment) what the function required of its arguments, and then we checked that this requirement held (throwing an exception if it did not).

This is a good basic strategy. A requirement of a function upon its argument is often called a precondition: it must be true for the function to perform its action correctly. The question is just what to do if the

precondition is violated (doesn’t hold). We basically have two choices:

[1] Ignore it (hope/assume that all callers give correct arguments).

[2] Check it (and report the error somehow).

Looking at it this way, argument types are just a way of having the compiler check the simplest preconditions for us and report them at compile time. For example:

Click here to view code image

my_complicated_function(1, 2, "horsefeathers")

Here, the compiler will catch that the requirement (precondition) that the third argument be an integer was violated. Basically, what we are talking about here is what to do with the requirements/preconditions that the compiler can’t check.

AA

Our suggestion is to always document preconditions in comments (so that a caller can see what a function expects). A function with no comments will be assumed to handle every possible argument value. But should we believe that callers read those comments and follow the rules? Sometimes we have to, but the “check the arguments in the callee” rule could be stated, “Let a function check its preconditions.” We should do that whenever we don’t see a reason not to. The reasons most often given for not checking preconditions are:

Nobody would give bad arguments.

It would slow down my code.

It is too complicated to check.

The first reason can be reasonable only when we happen to know “who”

calls a function – and in real-world code that can be very hard to know.

The second reason is valid far less often than people think and should most often be ignored as an example of “premature optimization.” You can always remove checks if they really turn out to be a burden. You cannot easily gain the correctness they ensure or get back the nights’ sleep you lost looking for bugs those tests could have caught.

The third reason is the serious one. It is easy (once you are an experienced programmer) to find examples where checking a precondition would take significantly more work than executing the function. An example is a lookup in a dictionary: a precondition is that the dictionary entries are sorted – and verifying that a dictionary is sorted can be far more expensive than a lookup.

Sometimes, it can also be difficult to express a precondition in code and to be sure that you expressed it correctly. However, when you write a function, always consider if you can write a quick check of the preconditions, and do so unless you have a good reason not to.

4.7.3.2 expect()

Writing preconditions (even as comments) also has a significant benefit for the quality of your programs: it forces you to think about what a function requires. If you can’t state that simply and precisely in a couple of comment lines, you probably haven’t thought hard enough about what you are doing.

Experience shows that writing those precondition comments and precondition tests help you avoid many design mistakes. We did mention that we hated debugging; explicitly stating preconditions helps in avoiding design errors as well as catching usage errors early. Writing

Click here to view code image

int my_complicated_function(int a, int b, int c) // the arguments are positive and a < b < c {

if (!(0<a && a<b && b<c)) // ! means "not" and && means "and"

error("bad arguments for mcf");

// ...

}

saves you time and grief compared with the apparently simpler Click here to view code image

int my_complicated_function(int a, int b, int c) {

// ...

}

Following the advice of checking preconditions soon leads to a couple of problems:

Some preconditions cannot be checked simply and cheaply. For those, stay with the comments and check only what can be checked simply and cheaply.

We can’t see whether an if-statement checks an invariant or is part of the ordinary logic of the function.

To deal with the second problem, we introduce a function called expect to do the checking. As arguments expect() takes a function to test and a string used to report errors:

Click here to view code image

bool ordered_positive(int a, int b, int c) {

return 0<a && a<b && b<c;

}

int my_complicated_function(int a, int b, int c) // the arguments are positive and a < b < c {

expect(ordered_positive(a,b,c), "bad arguments for mcf");

// ...

}

now we can see what my_complicated_function() expects from its arguments, but for tests we don’t use often in our source code, we’d rather have them expressed directly where they are used. We can do that:

Click here to view code image

int my_complicated_function(int a, int b, int c) // the arguments are positive and a < b < c {

expect([&]{ return 0<a && a<b && b<c; }, "bad arguments for mcf");

// ...

}

The construct

Click here to view code image

[&]{ return 0<a && a<b && b<c; }

is called a lambda expression and will be explained in §21.2.3. The syntax isn’t as clean as we might like, but it constructs a function that tests

0<a && a<b && b<c

for expect() to call. If that test fails, Click here to view code image

error("bad arguments for mcf")

is called exactly as when we used the named function ordered_positive().

4.7.3.3 Postconditions

Stating preconditions and inserting calls to expect() help us improve our design and catch usage errors early. Can this idea of explicitly stating

requirements be used elsewhere? Yes, one more place immediately springs to mind: the return value! After all, we typically have to state what a function returns; that is, if we return a value from a function, we are always making a promise about the return value (how else would a caller know what to

expect?). Let’s look at our area function (from §4.6.1) again:

Click here to view code image

int area(int length, int width) // calculate area of a rectangle // the arguments are positive {

expect([&]{ return 0<length && 0<width; }, "bad arguments to area()");

return length*width;

}

It checks its precondition, but just assumes that the computation is correct (that’s probably OK for such a trivial computation). A check on the

correctness is called a postcondition. For area() the postcondition would be that the result really was the area. We can’t check the complete postcondition, but we can check that the result should be positive:

Click here to view code image

int area(int length, int width) // the arguments are positive {

expect([&]{ return 0<length && 0<width; }, "bad arguments to area()");

int a = length*width;

expect([&]{ return 0<a; }, "bad area() result");

return a;

}

This code looks rather bloated compared to the straightforward Click here to view code image

int area(int length, int width) {

return length*width;

}

but it illustrates a technique that can be enormously useful when writing

programs where correct results are critically important.

Try This

Find a pair of values so that the precondition of this version of area holds, but the postcondition doesn’t.

Preconditions and postconditions provide basic sanity checks in code. As such they are closely connected to the notion of invariants (§8.4.3),

correctness (§3.2, §4.2), and testing (§4.7.4).

CC

Note that error() and expect() are not part of the ISO C++ standard library.

They are just part of the PPP_support module. There is work going on in the standards committee for direct support for preconditions and postconditions, but at the time of writing, that work is not complete.

Dalam dokumen ePUB eBook Customization Guide (Halaman 174-180)