Journal Articles
Browse in : |
All
> Journals
> CVu
> 136
(8)
All > Journal Columns > Code Critique (70) 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: Student Code Critique Competition 13
Author: Administrator
Date: 03 December 2001 13:15:48 +00:00 or Mon, 03 December 2001 13:15:48 +00:00
Summary:
Body:
prizes provided by Blackwells Bookshops & Addison-Wesley
Note that if anyone has not received a promised prize they should contact me (Francis.glassborow@ntlworld.com) so that I can see what has gone wrong because I believe that I am now up to date.
I had a very small response to the last piece of code and that worries me. The code had quite a bit that merited comment, so how was it that so few of you entered? In fact I had to ask one of my friends to provide a critique, though soon after he did so I had a genuine entry.
Let us start with the code:
// From Borland C++ 4.5 page 100 Demonstration // of loops using Newtons Method of finding // square roots #include <iostream.h> const double TOLERANCE = 1.0e-7; double abs(double x) { return (x >= 0) ? x : -x; } double sqroot(double x) { double guess = x /2; do { guess = (guess + x / guess) / 2; }while(abs(guess * guess - x ) > TOLERANCE); return guess; } double getNumber() { double x; do { cout << "Enter a number: "; cin >> x; } while (x < 0); return x; } main(){ char c; double x, y; do { x = getNumber(); y = sqroot(x); cout << "Sqrt (" << x << ") = " << y << endl << "Enter another number? (Y/N) "; cin >> c; cout << endl; } while (c == 'Y' || c == 'y' ) ; return 0; }
Walter Milner< w.w.milner@bham.ac.uk>
I'm going for the 'most terse entry' prize -
-
The comment - it is pedagogically unsound to use this to demonstrate loops - there are far too many distracting issues it raises
-
Globals should be avoided wherever possible - so make TOLERANCE local to sqroot
-
Prototype functions
-
Use double abs(double) from <cmath>
-
Use / 2.0 in sqroot()
-
Use x < 0.0 in getNumber
-
Trap x==0.0 in sqroot to avoid divide by zero if user enters 0
-
Declare main as int main(void)
-
The student should compare this to pow(double, double)
I have no problem with terse entries as long as they are substantially complete and correct. The problem with Walter's submission is that he seems to have missed that most of the code is floating point code (e.g. his choice of abs instead of fabs(). He correctly tries to trap divide by zero but his trap will not deal with the overflow potential in C.)
Oh, and prototypes are not needed if the definition is visible at the time of use (i.e. the definitions are in the same file and before the first call of that function).
Good try, but not enough to get a free book.
As Francis had very few entries for this competition he has persuaded me to provide an entry. It is under the strict condition that I must not be the winner. Some of my comments may be a little unfair because I think the material was extracted from an older book, however I am going to pretend for the sake of this review that it was written yesterday. I will stick to my usual format of working through the code as it was provided.
#include <iostream.h>
Well we can argue about this one. There is nothing wrong with that as a header file as long as the author does not claim that it is a Standard C++ header. It is not and never has been. The correct header file for Standard C++ i/o is iostream (note: no '.h') which places everything in namespace std. To keep things simple I will assume that the actual first two lines were:
#include <iostream> using namespace std;
There is nothing exactly wrong about this next line:
const double TOLERANCE = 1.0e-7;
but there are two things that I would do differently. I never use identifiers that are devoid of lowercase letters if they are not provided by the pre-processor (I never use lowercase letters in preprocessor identifiers). I also try to stick firmly to writing 'const directly after the type being qualified. Neither of these things is a major issue so do not waste time arguing about it.
double const tolerance = 1.0e-7;
However that tolerance seems somewhat generous if you are computing with doubles. It also suffers from being an absolute value which is hardly useful if you are calculating square roots of smallish values. That is a serious fault because it makes a quite unwarranted assumption.
This next function caused me considerable surprise. What is it for? Yes I know what it does, but why has the author written it. Only someone who was substantially ignorant of what the C Standard Library provides would waste time writing something like thi (particularly with a reserved name in C).
double abs(double x) { return (x >= 0) ? x : -x; }
So those three lines should be replaced by:
#include <cmath> // Use double fabs(double) from the library
Now the next bit looks OK though we will have to patch it up to handle the poor choice of the value for tolerance and to replace abs with fabs.
double sqroot(double x) {
Note that the writer assumes that x will be positive. We need to add a line:
if(x < 0) throw range_error( "cannot find square root of negative number")
Which means we will also need to add:
#include <exception> double guess = x/2;
and we better add something to mend that tolerance calculation by scaling it to the actual value of x:
double const tolerance = ::tolerance*x;
Yes, you are right, I am being lazy and should really use another name, but it reminds people of the way we can use a local version of such a value which depends on the global one.
do { guess = (guess + x / guess) / 2; }while(abs(guess * guess - x ) > TOLERANCE);
Patching that line gives us:
} while (fabs(guess * guess - x) > tolerance); return guess; }
I assume that the rest of the code is a test harness to demonstrate that the above function works. I hope that you hate functions that continue for ever without giving you a hint that you are not providing what you want. Imagine the poor user who enters -1. It is a perfectly good number and nowhere has this function stated that it only accepts positive floating point numbers. Actually, I consider this sort of function the kind of utility function that will handle the other kind of input error where the user types something like "minus one". Now the function can never end.
double getNumber() { double x; do { cout << "Enter a number: "; cin >> x; } while (x < 0); return x; }
The above function should be replaced with something like:
double getdouble(char const * message){ int retries 3; while(retries--){ cout << message ; cin >> x; if(!cin) { cout << "invalid input found \n"; while(cin.get() != '\n'); cin.clear(); } else return x; } throw exception("repeated invalid input from console") }
A bit more polish would be nice but the above should give you a fair start.
main(){ char c; double x, y; do { x = getNumber(); y = sqroot(x); cout << "Sqrt (" << x << ") = " << y << endl << "Enter another number? (Y/N) "; cin >> c; cout << endl; } while (c == 'Y' || c == 'y' ) ; return 0; }
Well this is what I would write to achieve more or less the above test functionality. First we need an input function to deal with the yes/no question and ensure the input stream is left in a clean state:
bool yesno(char const * message) cout<< message; char c = cin.get(); // flush the input while(cin.get() != '\n'); return(c=='Y' || c=='y'); } int main (){ // note the original forgot 'int' try { do { double x = getdouble( "Type in a positive number"); if(x,0){ cout<< "A positive number," << " input ignored \n"; continue; } double y = sqroot(x); cout << "Sqrt (" << x << ") = " << y << endl; while (yesno( "Enter another number? (Y/N): ")); } catch(exception e){ cout << e.what(); } return 0; }
There are quite a few rough edges in my code that could be further smoothed. For example I have twice flushed the input stream, it would be worth writing a simple inline function to do this. However I think that this code is a great improvement on the original though it does not take much more space.
I suspect the lack of entries to this competition is because many did not see the faults. Training your eye to recognise poor code is a valuable skill. If you did not see at least half the flaws in the above code then you need to hone your code inspection skills, you are too trusting of published code.
There are major and minor problems with the code as written. When looking at the code I have tried to bear in mind the nature of its presentation and what might be expected from an introductory text.
The program doesn't start well when the old-style iostream.h header file is used instead of the C++ standard header iostream. The iostream.h header is really only provided for backwards compatibility and most new code should be using the standard headers. The contents of may be non-standard and non-conformant, whereas iostream is portable and conformant. The iostream header puts the definitions into the std namespace whereas iostream.h puts them into the global namespace.
The next problem is the definition of the abs() function. The author could have used the fabs() function, but this would have meant including math.h (to be consistent with his usage of headers). To be fair, the mathematical functions may not have been introduced at this stage so to avoid confusing the reader with side-issues a simple abs() function was created. In general though, library functions should be used where possible.
The other faults are:
-
There are no function prototypes. The code relies on functions being declared/defined before main().
-
There is little error checking. For example, the code will fail with a divide by zero error if the square root of zero is calculated.
-
The program will fail to meet the tolerance criteria if a very large value is entered. Convergence criteria are always difficult and generally require some thought.
-
A lack of comments - although these may have been incorporated into the text of the book.
On a stylistic note, the author uses two different methods for retrieving input from the user, after a prompting message. The getNumber function and the cout/cin method. This is clearly calling for refactoring and is a suitable candidate for a template - although again in an introductory book this might be considered too advanced. I do however feel that the author should have implemented getChar in this instance.
The author's use of cin to retrieve numbers and characters from the user may be problematic as no care is taken to flush the input stream or remove extraneous newline characters. The use of cin.ignore() might be overly complicated for that stage of the book.
The VC++6 compiler does not add functions/values from climit or cmath to the std namespace. Actually as most of the values in climit are likely to be macros they would be added to the global namespace. Although the MSDN documentation implies that the contents of the c* headers are added to the std namespace, an attempt to use std::fabs() will fail to compile. If you are using a more conforming compiler then you may have to use std::fabs. An alternative is to use using namespace std; so that we do not have to worry about which functions are in std and which are in global namespaces - but I receive the impression that this is regarded as poor form. (It sort of depends where you place such directives)
A template function to retrieve input from a user has been implemented. This is my first attempt at using a template so there may be implementation issues (like its position) that an expert might like to comment on.
The sqroot function has multiple return points. While some programmers regard this as a bad thing, I think it makes sense to implement early exit conditions separately from the main code of the routine. While it could be rewritten using nested condition checking, I believe that my form is acceptable.
The sqroot function implements a return status through the ifail parameter. Some readers may detect an exposure to the NAG library philosophy here. I felt it was better to pass back a status rather than rely on special return values from the main function.
The sqroot function implements convergence checking by using a ratio rather than an absolute comparison of the difference between the calculated and required values. I believe that this caters better then the original code for a wider range of input. In production code, a number of different convergence criteria may be required for different ranges of input values.
The whole of this column and Catriona's code should be posted to our website. While I try not to split articles, the existence of a centre page spread this time means that I need to. You wil find the code for the next competition on the last page of this issue.
Set by Francis Glassborow
Prize provided by Blackwells Bookshop in co-operation with Addison-Wesley
Entries in by January 14 (send to francis.glassborow@ntlworld.com)
The following code is intended to work out the value of a blackjack hand. There is a specific problem with counting the number of aces. In addition there are numerous places where the general structure of the code could be improved.
#include<cctype> #include<iostream> using namespace std; int card_val(int card_no); int number(int num_cards); int ace_count(int& aces); int hand_value(int total, int aces); int main(){ int card_no, num_cards; char again; int total=0; int aces=0; cout<<"\nWelcome to the " <<"Blackjack Counter.\n"; do { num_cards=number(num_cards); for(card_no=1; card_no <= num_cards; card_no++) total+=card_val(card_no); if(total<=21) cout<<"\nYour hand is: "<<total<<"."; else { total=hand_value(total, aces); if (total<=21) cout <<"\nYour hand is:" <<total<<"."; else cout <<"\nYou busted with " <<total<<"!"; } total=0; cout<<"\nDo you wish to continue?"; cout<<"\nEnter 'Y' (Yes) or 'N' (No): "; cin>>again; } while ((again == 'Y') || (again == 'y')); return 0; } int number(int num_cards){ do { cout<<"\nHow many cards do you have? "; cin>> num_cards; if ((num_cards <=1) || (num_cards >=6)) cout<<"You must have between" << " 2 and 5 cards (inclusive)."; } while((num_cards <=1) || (num_cards >=6)); return num_cards; } int card_val(int card_no){ char card_val; bool invalid_card; int value; int aces =0; do { invalid_card=false; cout<<"Enter value of card number " <<card_no<<": "; cin>>card_val; switch (tolower(card_val)){ case 'a': value=ace_card(aces); break; case '2': value=2; break; case '3': value=3; break; case '4': value=4; break; case '5': value=5; break; case '6': value=6; break; case '7': value=7; break; case '8': value=8; break; case '9': value=9; break case 't':value=10; break; case 'j':value=10; break; case 'q':value=10; break; case 'k': value=10; break; default: cout<<"\nNot a valid entry." <<" Please try again.\n"; invalid_card=true; } } while (invalid_card); return value; } int ace_count(int& aces) { aces++; return 11; } int new_value (int total, int aces){ while ((total>21) && (ace_count>=1)) { aces--; total-=10; } return total; }
Notes:
More fields may be available via dynamicdata ..