Journal Articles
Browse in : |
All
> Journals
> CVu
> 135
(7)
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 12
Author: Administrator
Date: 03 October 2001 13:15:48 +01:00 or Wed, 03 October 2001 13:15:48 +01:00
Summary:
Body:
prizes provided by Blackwells Bookshops & Addison-Wesley
I had a fantastic response to last issues piece. Not only was the number of responses the highest ever but also the quality was outstanding with surprisingly little overlap. That is why I have little hesitation in publishing so many. I wish that it were easier to drag this talent into the open. I know how I hated writing essays when I was at school, yet as I have aged I have found a peculiar satisfaction in sharing my ideas in writing. This is equalled by the satisfaction I get from reading the ideas and insights of others.
If you want me to continue to run this item as a way of helping my successor, you know what to do, don't you?
Now let me start with a contribution based on competition 10.
Before providing my own solution, I'd like to raise some points about the solution presented in the August 2001 edition of C Vu. Page 7 column 1: a header file is presented with the following points:
iostream.h is included directly in the student header file. This is unnecessary - the ostream class could simply be forward-declared [only if you have a library that provides <iosfwd>, in general you are not allowed - read should not expect to work - to forward declare C++ Standard Library components other than by inclusion of the designated header file. This may seem unreasonable until you realise the degree of interaction between these components, particularly those that are templates. Ed] This issue turns up again with the "new style" C++ solution offered later on, although the solution is slightly different; more later (point 3(d)).
The MAX_ENTRIES constant is not scoped - it is a global value, similarly for the typedefs
Page 7 column 2: a continuation of the header and an implementation file are given, with the following points:
The default Student constructor is placed inline in a header. Whilst this is perfectly valid, I do not generally place any code in header files for the simple reason of compile-time dependencies; it is bad enough having such dependencies on the interface only without adding the further burden of making clients of this class also compile-time dependant on the class implementation. One notable exception to this is, unfortunately, template code.
None of the constructors make any attempt at initialising the contained arrays to safe values. At a minimum I would have expected something like:
CStudent::CStudent() : m_numGrades(0){ ::memset(m_subj, 0, sizeof(m_subj)); ::memset(m_score, 0, sizeof(m_score)); }which is rather 'C'-like, but then so is the use of such an array. There are much better solutions to be found for storage of such data in C++ - more later.
However there are issues with doing this in that it assumes that the types of score_t and subjcode_t can be correctly zeroed by memset. This is not always true; for example it is incorrect for floating point types. Ed
Use of typedef - Francis commented on this. The specification required that both subject and mark are implemented as types. I understand by the use of the word "type" here to mean "user-defined type". Francis suggests the use of a struct. An alternative may be the use of an enumerated type (at least for the subject code) - which also meets the criteria of a user-defined type.
The use of std::endl on a generic ostream. The ostream could be a file, in which case you would most likely not want to use std::endl as it flushes the stream. A safer alternative is to use "\n"
The "add" method returns an int / discussion of returning index of how many items added. This is poor OO-design. The client of this class should neither know nor care whether the class stores the information in an array or any other sort of container, let alone be given knowledge of the location (index of the entry in the array in this case) where the data is stored. A simple bool would prove entirely adequate. Throw an exception? Well, why? Is it really an exceptional case or is it more a standard case? I suspect the latter, in which case again a simple bool is more than adequate.
Name of method: output. I try to avoid such names as "input" and "output" as they are so generic they lose all meaning in a precise context. A suggest made by Josuttis in his book The C++ Standard Library is to call the method something like printOn(). This, to my mind, describes more accurately what the method is trying to accomplish.
Page 8, column 1: more elegant ways of implementing the class / use of "newer" C++:
"Checking for valid input" etc. If the subject codes were taken from a fixed, pre-defined list (the assumption here is that the number of subjects is constrained to some finite number) then the validation for subject codes is implicit. Of course, in line with Peter Goodliffe's article elsewhere in the same edition of C Vu, you may wish to code an assert and/or a run-time check to ensure that subject code does not lie out of the expected range, although if the subject code is taken from a pre-defined (assumedly valid) list of subject codes this case should never arise.
Again, usual comments re: global typedefs, inlined default constructor etc. (as above).
The author has failed to take advantage of the typedef subjectlist for use in the subsequent typedef of the subjectlist iterator. There are therefore two points of maintenance here rather than one.
I did say in point 1(a) above I would return to the issue of #include vs. forward-declared classes. Now that the C++ Standard Library of <iostream> (as opposed to the earlier <iostream.h>) is being used, the correct technique for the header is to #include <iosfwd> instead of <iostream>. The <iosfwd> header file has been built to provide forward declarations of all the types together with their dependencies precisely for this situation.
The for() loop etc. given towards the end of the column needs tidying up. Here's the original
subjectiter iter = subjects.begin(); for( ; iter != subjects.end(); iter++ ) { os<<"Grade:" <<(*iter).first <<" Score: " << (*iter).second << std::endl;
Here's a tidied-up version:
subjectlist::const_iterator iter = subjects.begin(); const subjectlist::const_iterator iterEnd = subjects.end(); while(iter != iterEnd){ os <<"Grade:" <<(*iter).first <<" Score:" << (*iter).second <<"\n"; ++iter; }
The end() method is not repeatedly called around the loop and the iterator type is constant as the data held in the container is not modified. In addition, the iterator is incremented correctly in this case by means of the prefix, not the postfix, increment operator. The output stream is not flushed as I'm using "\n" rather than std::endl.
The output() method could (and should) be made const as it does not modify the object.
Page 8, column 2: use of std::vector<grade_t> and use the subject code as the index into the vector. This is a poor choice for these reasons:
It does not allow for subject codes being alphanumeric which is certainly a possibility even if not now but in the future (no information in the requirements is given on the nature of subject codes )
Even if the subject codes are numeric only, it would seem reasonable to assume that they would be allocated ranges per department/subject area e.g. 100 - 199 for natural science 200 - 299 for engineering, 300 - 399 for languages etc. Assuming the candidate took, say, 5 natural science courses and 5 language courses then you would have "wasted" 295 entries in the array, assuming the 5 language courses the candidate chose happened to be numbered 300, 301, 302, 303 and 304.
For both these points a map would appear to be a much better choice, keyed by subject code. Of course, there is nothing wrong with using some other container such as a list - although finding an entry for a given subject code is generally a comparatively slow business.
"If the subject code must appear more than once" - I would be inclined to treat this as an error condition (and one to check for in the add() method) as I think it unlikely a student would want to be enrolled on exactly the same course twice.
N.B. I purposely haven't attempted to catch any exceptions as I expect these to be handled a user of the class and that I haven't put any throw() declarations on any methods as my compiler doesn't support them.
Note that Paul's code can be found at http://www.accu.org.
As per the requirement, both the Subject and Grade have been made into user-defined types (structs). This is, in my opinion, a somewhat odd stipulation to be placed in the requirements specification as the requirements should be concerned with what the solution is required to do and not stipulate exactly how a particular data type is to implemented. In general, the requirements for this exercise are very vague and lacking in all sorts of detail necessary for a satisfactory solution. During the course of writing the proposed solution I found myself having to make a lot of assumptions about the data and how it was going to be used.
The Subject struct is straightforward and consists of a code string field for the subject code, a title string field which contains the full name of the course and a max_mark field which contains the maximum mark possible for that particular course examination. The assumptions I had to make here were:
-
the subject code could be alpha-numeric so I used a string instead of a numeric built-in type for the subject code
-
the subject code would be a shorthand for the real course and so there may well be a requirement to display the full course name for a given course code, so I added a title field
-
the maximum mark available for a given examination for a subject may not be 100. e.g. the exam may be marked out of, say, 70 in which case to get the percentage score for a given subject you would need to divide the actual score by the maximum possible and multiply by 100. Each subject may have a different maximum score and so a maximum score per subject belongs in the Subject.
None of these assumptions would have had to be made if the requirements were more complete. Part of this "coding" exercise therefore turned into trying to second-guess what was required. This is poor practice. Requirements should never be second-guessed as the guess is almost invariably wrong. If the requirements are unclear, the requirements should be clarified before proceeding. Sometimes, of course, it only becomes apparent during design or even implementation that the requirements are unclear. In these cases, clarification should be sought before progressing with the code. However, this is not practical for a coding exercise in a periodical.
I have made the Subject constructor take three parameters to initialise all the data members correctly. I have purposely included a default "empty" constructor for use with container classes that may require a parameterless constructor: consider:
std::vector<Subject> subjects(10);
This will call the default constructor for Subject ten times [actually, no, it calls the default constructor to create a temporary and then calls the copy constructor ten times. Ed] and. The member initialisation list for the default constructor and the overloaded constructor are different. This is because the default constructor for a std::string object is guaranteed to be an empty string. It is therefore unnecessary to put code_("") and title_("") in the default constructor initialisation list - especially as two temporary std::string objects will be created, copied and then destroyed and all with absolutely no beneficial effect on the newly constructed object.
The usual header include guards have been placed in each of the headers. I have used the convention of appending a trailing underscore to indicate class/struct data members. Another commonly employed method is to prepend "m_" to the variable name. I have used unsigned short [so you decided that it was all right to multiply marks by marks? Ed] as the data type for marks as I have assumed that a maximum value of 2^16 - 1 will cover all possible mark ranges. I could have typedef'ed the unsigned short to, say, mark_t but I did not see the need. Again, no mention was made in the specification of valid mark ranges or whether negative marks may be passed in. If a negative mark were passed in it would be implicitly cast into a large unsigned value turning, in most cases, a hopeless failure into an impossible success! If the user-interface performs simple validation on data input then we may safely assume the data lies in a valid range. Again, no indication in the specification is given as to whether this is the case or not.
The overloaded constructor for the Subject struct uses const references to the std::string class for its first two arguments. In the case where another std::string object is being passed, then this will ensure that a copy of the std::string is not needlessly created. For the case where the overloaded constructor is called with string literals (probably the more usual case) then two temporary string objects will be created and then a const reference to those temporary objects will be passed as arguments. The alternative is to use copies of the string objects. In this case, nothing is lost for the string literal case as two temporary objects would need to be created in the const reference case and two objects will be created in the case of argument copies. However, for the case where another string object is passed, an unnecessary copy will be made of each of the string objects. On balance, the use of the const reference would appear to be the preferred one. [actually, there is another option and that is to add overloads for the various combinations of string and char* (C-style strings). Ed.]
Despite my best efforts, I could not see any more use for the "Mark" struct than simply to contain a single value, which strikes me as rather unnecessary, although it could be argued that placing the value in a struct could be some form of "future-proofing" of the data type. Still, placing the single mark value inside a struct does at least meet the rather odd design constraint that both "Subject" and "Mark" should be user-defined types. The "Mark" constructor takes one parameter which is defaulted to zero. This allows the use of an empty constructor (e.g. as above for the std::vector of Subject) and it also allows the single value to be initialised, if required, with the same single constructor implementation. The constructor has been marked as "explicit" to stop unwanted implicit conversions.
[Let me explain that requirement. Types have appropriate behaviour. If you do not make Mark a UDT then it has inappropriate behaviour. You may wish to live with that initially, but in the long run you may well want to clean it up. Programmers are often reluctant to properly encapsulate data and so leave their data vulnerable to erroneous use. All type that have public visibility should have correctly constrained semantics. None of the built-in types behave correctly for marks, dates, weights etc. Ed]
Neither of the Subject or Mark structs have user-defined copy constructors, assignment operators or destructors as the ones supplied by default (the canonical form) are adequate for our purposes. The compiler-supplied destructors are non-virtual so if it is desired to inherit from these structs then a default destructor must be supplied and marked as virtual in the struct declaration. However, it is unlikely that Subject and Mark will be inherited from and so I allowed the default compiler-supplied non-virtual destructor to be used.
The NewStudent class is where the proposed solution starts to get a little more interesting. The header file contains forward declarations for all classes except std::string because there are two methods declared to return a std::string by value so the full header is required. Again, a default constructor is declared and const references to the two std::string arguments are also declared in the overloaded constructor for the same reasons given above for the Subject struct. A set of methods are declared which would appear to cover any reasonable use of the class. Again, assumptions had to be made as the requirement was so vague in specifying exactly what sort of operations would be required to be made available. Perhaps the most interesting part of the whole class is what you don't see: the only private member is a pointer to a forward declared class. I named the pointer member "pImpl" after one name for this technique, which is called the "pimpl idiom". It is also known as the handle/body idiom, the letter/envelope idiom and the compiler-firewall idiom, amongst others. I won't discuss this idiom here as it is well-documented in the extant C++ literature.
I have attempted to make the NewStudent class (and the implementation class thereof) as const-correct as possible. The encapsulation of the class is keep fairly tight partly by use of the handle/body idiom and partly by use of the printOn() method. However, the encapsulation is somewhat violated by use of accessor/mutator methods for the properties "code" and "name". This is because I wanted to provide a default, no-parameter, constructor for the NewStudent class such that it could be used easily in situations where a default constructor is required (e.g. in STL container classes). Whilst this particular NewStudent class is not used in a container of NewStudent by the test program it is not difficult to foresee a situation where it may well make sense to have a container of NewStudent. Therefore I provided a default constructor and the accessor/mutator methods. I would have preferred the single constructor containing two arguments as this would enforce in code what I would like to constrain as the semantics of using this class, namely that an object of this must have at least a student name and student code.
Unfortunately, for the reason given above, I had to provide a default constructor which means it is legal and possible for an object of this class to be created without giving it a student name and student code. It is up to the user of the class to remember to provide these two parameters afterwards if the default constructor is used. Given that I cannot express fully the semantic use of the class in the C++ implementation code, another mechanism would be to rely on another part of the system (e.g. user input screen) performing validation before a NewStudent object is created. This is more nebulous than specifying/coding that constraint directly in the class but it would enforce the semantics as part of the overall system. The default constructor could then be called by container classes when required.
For the NewStudent class I have provided implementations of the copy constructor and assignment operator as the NewStudent class contains a pointer member to another class. The correct behaviour for copying/assigning to this class from another object of the same class is to provide a deep copy for the pointer to the NewStudentImpl class. The NewStudentImpl class itself does not need a specialised copy constructor and/or assignment operator so the compiler-supplied default ones will be used.
Depending on the requirements, it may be an option to wrap the bare member pointer to the NewStudentImpl class in something like a shared_ptr smart-pointer class from the Boost library.
In general, I prefer to use the standard library algorithms for looping over containers. However, one case where I wrote my own loop is in the printOn() method of NewStudentImpl. There are two reasons for this:
-
one of the reasons the method printOn() is supplied is to avoid violating encapsulation. By providing a functor outside the scope of the class I would have to give it "friend" access to the class. This is not as bad as is usually thought: Marshall Cline makes out a convincing case for the fact that judicious use of "friend" can actually increase encapsulation rather than decrease it in his C++ FAQ. However, Josuttis discusses this very point in his book The C++ Standard Library and the recommendation there is to use a method such as printOn() - which I have followed here.
-
in displaying the student information, I make use of some member functions from the NewStudentImpl class. This would mean that a functor would have to know which object it is being called from.
One final point regarding the use of assert: although I have #included<cassert> I have had to use assert and not std::assert as I would have expected [The reason is that assert is a macro. Ed]. This is down to implementation of the cassert for my compiler. I have attempted to use assert in order to document pre-conditions for many methods: the majority of these being simple pointer checks before de-referencing when delegating from the NewStudent to the NewStudentImpl class. Although I perform a run-time check on these pointers, I also place an assert just before the run-time check to remind me that by design the pointer is supposed to be non-null rather than a run-time check which says "if it's null, don't use it". The two checks emphasise different concerns with the code.
This is just one implementation. No attempt has been made to address Unicode character encodings - although this could be achieved fairly easily by using the wchar_t version of std::basic_string and wchar_t where I have used char.
Although I would hope to have met the original requirement of providing a class to handle a set of marks and subjects for a given student, I do not feel that the solution above is necessarily a pragmatic one. This class of problem lends itself readily to a database implementation, which would seem to be the most natural way of implementing the requirements. However, I suspect a database solution is not what is required by way of a C++ coding exercise!
Good student exercises are capable of being implemented at many levels. This is a reason that good tutors under-specify coding exercises (bad ones do so because they do not know any better). Good students ask for further detail or make their assumptions explicit. That exercise is valuable in itself.
To return to the issue of UDTs; I think that much code makes poor use of these. Let me give you a simple example. How often have you seen length (in meters, or whatever) simply implemented as a double? Now it is quite correct to multiply a length by a floating-point value to get a length, but if you multiply a length by a length you get a unit of area (square metres etc.).
Much of the design of a program is selecting and implementing (if not already done) correct types. There are often surprises, you can subtract dates, but you cannot add them, you can add and subtract integers from dates but you should not subtract a date from an integer (should you be allowed to add a date to an integer rather than the other way round?)
It is questions like these that I would pose to my best students while allowing the weaker ones the satisfaction of writing a simpler program that works.
Then there is the issue of time scale. A perfect solution takes an eternity, but how good is good enough? Students should have to think about such things. These are issues that need to be addressed, and too often are not. Good enough for my own use is not the same as good enough for sale. Francis
Before we start to look at the code, let me briefly remind readers what Newton's method (or the Newton-Raphson method if you prefer) is designed to do. The method provides a means to estimate a root of a function in n dimensions, although we will consider only the limiting case of one dimension (i.e. one independent variable). In other words, at its simplest, Newton's method attempts to calculate a value of x for which f(x) = 0 is true. Newton's method tries to evaluate a change in an approximation of x that will produce a better estimate for the root. Expressed differently, we seek to find a solution such that f(x + dx) = 0 to within some arbitrary bounds. We can calculate the shift, dx, as -f(x)/f'(x), and therefore we must calculate numerical values for both the function and its derivative (I leave it to the mathematically deranged to prove this to themselves).
Let us firstly consider the functionality of the code before we address any imperfections in semantics or design. The program begins by getting an initial approximation for the root from the user (I will later mention some other improvements which might be employed). The program then enters the main calculation loop. Here, the current best guess for the root location is used to calculate a new value for x, which is, at least in most cases, a closer approximation to the root sought. When the best guess is precise enough, in this case within 0.000001 of the root, checked by comparing the deviate of the new and previous best guess, the estimation is printed and the program terminates.
The bulk of the work, however, is performed within three auxiliary functions. Firstly, NewtonRough() calculates a new best guess for the root, based on a previous estimate, x0, passed as an argument. It does this by calling function func() to calculate the value of f(x) for x = x0, and derivative() to calculate the value of the derivative for which x = x0. The quotient of the function and derivative (f(x)/f'(x)) is subtracted from the previous estimate x0 to evaluate the new estimate, which is returned to main(). If we go further and examine func(), when we put the various components together we find the following function is evaluated:
f(x) = 25/7 * ln(3750 * (|x| ^ 0.5)) - 1/(|x| ^ 0.5) - 6.
The same process for derivative() shows that
f'(x) = 1/2x * (25/7 + 1/(|x| ^ 0.5)).
To improve upon the code as set, I will present two programs; the first will correct some of the immediate problems with the code, and the second will show a more robust and flexible version to give a taste of what might be achieved with a little further work. I will also state up front, that the code is (I hope) compliant with C89, but doesn't attempt to use any new C99 features. I only have a passing knowledge of C99 having not worked with C in anger for a couple of years.
I have not, by and large, commented on spacing issues as it seems clear that this is mostly to fit the code onto the page, and in any case is largely a matter of preference. I will mention, however, a couple of details where I am not certain if it is just publication issue. So onto the code and some specifics, the first program is given in newton1.h:
#ifndef NEWTON1_H #define NEWTON1_H #define PRECISION 1E-6 #define MAX_TRIES 30 #define INPUT_BUFFER_LEN 50 #define CONSTANT1 25.0 / 7.0 #define CONSTANT2 3750.0 #define CONSTANT3 6.0 #define CONSTANT4 2.0 double Func(const double current_gues ); double Derivative(const double current_guess); double Reciprocal(const double val_to_recip); void NewtonMethod(double current_guess ); double GetNumber(void ); #endif
and newton1.c
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <errno.h> #include "newton1.h" int main(void){ double current_guess = 0; printf("Enter an estimate of the root\n> "); current_guess = GetDouble(); NewtonMethod( current_guess ); return (EXIT_SUCCESS); } void NewtonMethod(double current_guess){ int tries = 1; double next_guess = 0; for(tries = 1; tries <= MAX_TRIES; tries++){ errno = 0; next_guess = current_guess - (Func(current_guess)/Derivative(current_guess)); if(errno == EDOM) { perror("Domain error encountered calculating next root estimate\n"); break; } else if(errno == ERANGE) { perror("Range error encountered calculating next root estimate\n"); break; } else if (errno != 0 ) { perror("Unexpected error encountered calculating next root estimate\n"); break; } if(fabs(next_guess - current_guess) < PRECISION) { printf("Estimation of root %f\n", next_guess); break; } else if( tries == MAX_TRIES ) { printf("Failed to reach convergence after %d attempts\n", MAX_TRIES); break; } current_guess = next_guess; } } double Func(const double current_guess){ double positive_sq_root = sqrt(fabs(current_guess)); double ln_eval = log(CONSTANT2 * positive_sq_root); return ((CONSTANT1 * ln_eval ) - (Reciprocal(positive_sq_root)) - CONSTANT3); } double Derivative(const double current_guess){ double factor = Reciprocal(CONSTANT4 * current_guess); double reciprocal_root = Reciprocal(sqrt(fabs(current_guess))); return((factor * CONSTANT1) + reciprocal_root); } double Reciprocal(const double val_to_recip){ if(val_to_recip == 0) { errno = EDOM; return ( HUGE_VAL ); } return (1.0/val_to_recip); } double GetDouble(void){ char number_buf[INPUT_BUFFER_LEN]; char * end_ptr = number_buf; double user_value = 0; while (1) { fgets(number_buf, INPUT_BUFFER_LEN, stdin); errno = 0; user_value = strtod(number_buf, &end_ptr); if(user_value == 0 && end_ptr == number_buf){ printf( "Invalid number format, non-numeric input or underflow\n> " ); } else if ( errno == ERANGE ) { printf("Overflow in number parsing\n> "); } else break; } return (user_value); }
Let us consider the overall structure of the code first of all. The main changes for the first program are just to farm out some of the work to extra functions. Most significantly I have moved the main calculation loop from main() to NewtonMethod(). Personally I tend not to have anything but the most basic of startup code in main(), and in the original code, NewtonRough() was hardly earning its keep. The other new functions are Reciprocal() and GetNumber(), which are discussed further below.
There are also a couple of minor details that are related to my personal preference which are worth noting. I have capitalised the first letter of each function (except, of course, main()) for consistency. As it happens, I tend to capitalise function names, but whichever way you prefer to name functions, I feel it ought to be consistent. I have also made all the function returns consistent. Again, this is a matter of preference; I always put the return in parentheses, but put a space between the return keyword and the first parenthesis to indicate it is not a function. This is one of those places where I am not sure if the inconsistency in the original code was a space issue, but I felt it worth flagging.
So onto the more significant details, roughly in the order they appear as you follow the program execution. The first issue is in the definition of main() itself. To my knowledge, unless it is different in C99 (with which I am as yet not totally familiar), in ISO C, main should be defined as one of:
int main(void)
or
int main(int argc, char *argv[])
of which the first form is preferred here as we are not using command line arguments.
I have added declarations for the user-defined functions as part of a header file. These are not required, and the code works fine without them, with the compiler treating the definitions as prototypes. That said, once larger software is considered, it is wise to have a declaration for every function (even if you do not intend to call the function outside of its translation unit in my opinion), so this is a good habit to develop. I like to give the compiler every chance to spot my mistakes.
As well as function declarations, I have declared the parameters as const where they are not modified. Once again, in a small piece of code, this makes little difference, but on a larger scale, it enables you to see the intent of the designer, and gives the compiler a chance to stop you doing something in error.
Throughout the code, I have renamed many of the variables to make them somewhat more meaningful. This adds clarity to the designer's intentions and makes the life of future maintainers (the original designer included) much simpler. Another potential benefit of using better names is revealed by a couple of typographical errors in the code. Those who only glanced over it, may have missed the upper case identifiers used by mistake in a few places (I wonder if these were deliberate or accidental on Francis' part?). To my mind, it is less easy to make this kind of mistake with more expressive names, as you tend to think about them more as you type, and once you make the mistake, it is visually more obvious.
The user input facility has been removed into a separate function, and made more robust. Trying to make decent input code with scanf() is notoriously difficult, and I guess many would say, impossible. The first issue with the scanf() statement as published, is that %f is the wrong conversion specifier for type double. Unintuitively, while %f is correct for double in printf(), %lf is used in scanf(). This is just one of the problems with scanf(). The second problem which might be encountered here is when some kind of sanity check is placed on the input as I have done. Unfortunately scanf() doesn't remove unexpected non-numeric data from the input stream, so any attempt to call scanf() again if an error is detected results in scanf() picking up the same bad character. Anyway, if anyone wants to know more about the problems, I am sure a browse through the accu-general archives will probably help, or those of comp.lang.c if it doesn't.
Furthermore, by rewriting the user input code to use strtod() from an input string, we can handle any numeric input (at least within the overflow and underflow boundaries for a double type), and easily implement a basic sanity check. The only drawback being, at least as far as I understand, that it is not possible to distinguish between an underflow and an invalid numeric format in a strictly ISO conforming library. Others of course, may, and probably will, know better?
Throughout the code, I have replaced 'magic' numbers with preprocessor macros. This is probably the most idiomatic way to deal with magic numbers in C, so is the method I have chosen. Not only is this more elegant and less error prone during later maintenance, but in some cases faster as it avoids a function call. In the case of the constants in the equation, the solution is still unsatisfying, however, as I couldn't think of great names for them. When we look at a more advanced version of the program, we will see a better solution. I have also factored out the taking of reciprocals into a function (it could equally well be a macro) so the multiple occurrences of the 'magic' number 1.0 are avoided.
Another problem can be identified from a consideration of Newton's method. It is not always foolproof in various circumstances, and no method is bulletproof in the face of a rootless function. With my maths being a little rusty I am not certain, but I believe the function implemented in the sample doesn't have any roots, only a singularity at x = 0. As a safety net, therefore, the calculation loop in main() is limited to a maximum number of tries, defined in this case to be 30. This is sufficient for most purposes as Newton's method converges rapidly in general.
Whilst the nature of the function under consideration removes the risk of encountering many of the possible errors in the standard C maths library, some can still occur. In this code, the log() function can produce a range error if the value is close to zero. By checking the external errno variable each loop cycle, we can handle this error, and also implement error checking in the Reciprocal() function.
A final change in main() is the return value to EXIT_SUCCESS. Whilst a return of 0 is perfectly valid ISO C, I prefer the standard macros, especially if the code is for the benefit of people relatively inexperienced. After all, how many people, inexperienced or otherwise, have occasionally parsed code such as the idiomatic (although perhaps unwise)
if(!(strcmp(s1, s2))) { ... }
as meaning 'if string s1 is not equal to string s2'? Although the return value of 0 for success in many situations makes sense to the experienced C coder, it is perhaps a little unintuitive for the unwary.
So this code deals with the basic aspects of the problem. We have tidied the code up so that it can be compiled, and is reasonably idiomatic standard C (well standard C89 anyway). The overall design is still very limited and lightweight, however.
For those who have managed to read this far, I have also written a somewhat larger and more flexible version of the software. This has slightly fewer rough edges, and can be adapted to wide range of functions with ease. It still maintains a pretty lightweight approach, but I hope is developed enough to be usable in real root finding problems. If a student really wanted to pursue the issue, they could add a fully-fledged parser, e.g. a recursive descent system, to the code I present below, but that is really beyond the scope of this article. The modified code is presented in newton2.c and newton2.h:
(For reasons of space the code has not been printed but is available on our web site, www.accu.org. - ed.)
All I have done there is break the concept of a mathematical function down into components separated by + and -, as one tends to while differentiating by hand. I have illustrated the mechanism with a simple quadratic equation, f(x) = 6x^2 + 9x - 6, which has two roots at -2 and -0.5, rather than the more complex function used before. I have created three calculation functions for the quadratic, linear and constant components of this equation, but it is a simple task to add further functions for other standard differentials. Each calculation function needs to be able to handle the calculation for both f(x) and f'(x) based on an enumerated mode parameter passed as an argument. With a library of standard calculation functions in place, to work with different equations all the user has to do is alter the static array of structures in CalculateBetterApprox() to use the right constants and calculation function. The op parameter of the structure determines whether the component is to be added to or subtracted from the evaluation, or in the case of op_end, indicates that the end of the equation has been reached. To create functions to handle more complex standard differentials we might need to add further constants to the structure, but again, this is a simple task.
This still isn't the most flexible system, but represents a significant improvement on the single function program originally outlined. This code can at least be used reasonably successfully in a real world situation, and could be extended to be friendlier if required. For example, it would be relatively trivial to add functionality to read the equation details from a file into a dynamically allocated list rather than having to recompile each time.
Finally we can briefly mention a few other additions that could be used to make the system more flexible. Adding a search facility to locate an initial estimate for a root rather than relying on the user might be useful. Other improvements might revolve around combining Newton's method with other root finding algorithms to improve global convergence and handle cases where one particular method runs into difficulties.
I thought Francis has chosen quite an interesting problem for the code critique competition. It makes a change to have a numerical example and so I thought I would give it a whirl.
The first thing I did was to find out what Newton's method is for finding the roots of an equation. It turns out to be quite simple, in theory anyway. First of all, you guess where you think a root of the equation might be. Then you plug your guess into Newton's formula and it gives you a better approximation to the root of the equation. Newton's formula is
xn+1 = xn - f(xn)/f'(xn).
Not horrendously complicated and so I thought I could, at least, get the student's program working. As you have probably guessed by now it was not all plain sailing. Lets tackle the easy bits first.
As it stands, the student's code won't even compile with out errors. It is a simple matter, even for the student, to find and correct the syntax errors and so I list them here with a brief description of the solution.
The parameter of the derivative function is a lower case 'x'. The body of the derivative function makes references to an upper case 'X'. I suggest changing the code to refer to the lower case 'x'. Lower case letters are normally used for depicting unknowns in mathematics.
The if statement within main has a missing closing bracket. One way to check that an expression does not have mismatched parentheses is to count the number of opening and closing brackets. There should be an equal number of each. It does not confirm that you have the correct number of brackets or that they are in the right place, but it can be a help.
The last parameter of the penultimate statement of main should be x0 (a lower case 'x'). I am sure that I am just as bad as anybody at making typing errors but you just have to sit back and read through your code carefully and methodically. See Pete Goodliffe's Professionalism in Programming #9, second bullet point entitled 'Don't code in a hurry'.
I think the code should compile now, but there are still one or two outstanding issues. The printf and scanf formatting strings should include a lower case l between the % and the f, i.e., the string should be %lf. This is because we wish to print and read variables that have been declared as double as opposed to float. All this business of defining the type of variable that is to be printed or used to receive a numerical value is, in my view, not very satisfactory. You just have to make sure you have the correct format specifier for the type and number of variables. Get a good book or read the Help files that come with the compiler. I find formatted input and output troublesome when programming in C. C++ has a different method of providing formatted input and output that knows the types of variables are being used. It is much safer and easer to use.
There are other oddities, such as evaluating pow(10.0, -6.0). I am sure this would work but it is a lot simpler to write the constant 1e-6 in its place. Also the student has created a loop by means of an empty for statement and has used the break statement to leave the loop. Although this has the desired effect, it is usually better (because in is easer to understand) to use a while loop or, more appropriately in this case a do while loop. The logical condition of the do while loop being true if function has not yet converged sufficiently. An example of the do while loop is shown in my version of the code, described later.
It is often stated that complicated expression should be split up into smaller ones. While this is probably true, it should not be taken to extremes as the meaning of the expression will be lost (for the human reader, that is). I think this has happened with the two functions func() and derivative(). In fact I would go so far as to say a single statement should represent each function. So, the body of func() should be
return 25.0/7.0 * log(3750 * sqrt(fabs(x))) - 1.0/sqrt(fabs(x)) - 6.0;
I have no idea what the expression means but at least I can see it all in one go. The same goes for the body of derivative(). Incidentally, I would not worry too much about repeating small sub expressions such as sqrt(fabs(x)) as the compiler will optimise common sub expressions (if the appropriate compiler options have been set). It is more important that the code is easy to read.
Now we come to the difficult bit, making the program do what it is supposed to do. Even when all the problems with the student's program have been put right it would not produce the correct result. In fact the estimate of the root kept on getting larger for each iteration. Eventually the value of the root became too large to be represented in a double. The program came crashing to a halt.
After some investigation, I decided that the function the student was using was not all that willing to be solved by Newton's method. I think it has something to do with the derivative having a very small value near the root. This has a tendency to cause the function to diverge rather than converge on a solution. The only way I could get Newton's method to converge was to enter an initial estimate very close to the root, a value greater than 1e-6 and less than 2e-2 seemed to provide the desired result and give a root at x = 0.005122. It is a pity that the student was given such a troublesome function to start with. A better-behaved function would, at least, encourage the student to get the basic program working correctly. Only then would it be appropriate to test Newton's method with more wayward function.
With the hints and tips I have provided, the student should be able to correct the problems with the code and get the program working. For this reason I do not provide a corrected version of the student's program. Instead, I thought I would have a go at writing a program to find the roots of a whole class of functions, not just one specific one.
As well as the function to be solved, Newton's method requires the derivative of the function to be known. In the case of the student's program this was entered directly in the code. Working out the derivative of functions in general can be quite daunting for anybody with my level of skill in mathematics. Even if the derivative can be found it has to be entered into the code, along with the original function. The program, therefore, can only solve one particular equation. To solve another equation the program will have to be edited to include the new equation and its derivative and recompiled. There is one important class of function that has a fixed structure and for which its derivative is easily calculated, even by a simple program. The class is that of the polynomial, specifically
p(x) = a0 + a1x + a2x2 + ... + anxn = 0.
The derivative is simply
p'(x) = a1 + 2a2x + ... + nanx(n-1).
The two equations can be rewritten in the form of nested multiplication for the sake of computational efficiency.
p(x) = a0 + x(a1 + x(a2 + ... + x(an-1 + anx) ...) p'(x) = a1 + x(2a2 + x(3a3 + ... + x((n-1)an-1 + x(nan))...)
The nested form of p(x) requires only n additions and n multiplications. The original form of p(x) requires n additions and 2n - 1 multiplications. Similar savings are also made for p'(x).
With a little thought it can be seen that, given an array of coefficients, both p(x) and p'(x) can be calculated by means of a looping structure. This had been adopted in the following program for finding the roots of polynomial equations using Newton's method.
#include<stdio.h> #include<math.h> double coefficient[10]; double estimate; int order; char reply; int get_order(void); double get_coefficient(int); double get_estimate(void); double evaluate_function(void); double evaluate_derivative(void); int main(void){ int i; double f; double d; double new_estimate; int iterations = 0; order = get_order(); for (i = 0; i < order + 1; ++i) coefficient[i] = get_coefficient(i); do{ new_estimate = get_estimate(); do{ estimate = new_estimate; f = evaluate_function(); d = evaluate_derivative(); new_estimate = estimate - f/d; ++iterations; } while (fabs(new_estimate - estimate) > 1e-10 && iterations != 50); if (iterations == 50) printf("The function did not converge within %i iterations.\n", 50); else printf("Root located at x = %.10lf\n", new_estimate); printf("Would you like to enter another initial estimate? "); scanf("%c", &reply); } while (reply != 'n' && reply != 'N'); return 0; } int get_order(void){ int order; puts("The program attemts to solve the polynomial equation"); puts("a0x^0 + a1x^1 + ... + anx^n = 0"); puts("where a0 .. an are the coefficients and n is the order of the polynomial"); printf("\nEnter the order of the polynomial: "); scanf("%d", &order); return order; } double get_coefficient(int i){ double coefficient; printf("Enter the value of coefficient a%d: ", i); scanf("%lf", &coefficient); return coefficient; } double get_estimate(void){ double estimate; printf("Enter your initial guess: "); scanf("%lf", &estimate); return estimate; } double evaluate_function(void){ int i; double f = coefficient[order]; for (i = order - 1; i > -1; --i) { f *= estimate; f += coefficient[i]; } return f; } double evaluate_derivative(void){ int i; double d = coefficient[order] * order; for (i = order - 1; i > 0; --i) { d *= estimate; d += coefficient[i] * i; } return d; }
When the program runs, the user is asked to enter the order of the polynomial followed by the coefficients. The user then enters an estimate of the location of root to be found. If a solution is found within 50 iterations the location of the root is displayed. If no solution is found a warning message is displayed.
Like most demonstration programs, my program can be improved in many ways. Probably the most dangerous aspect of the program is that there is no check on the order of the polynomial. A high order (greater than 9 in this case) will overrun the end of the coefficients buffer with, no doubt, disastrous results. Characters entered by the user are not checked to allow only those that are valid. To correct these defects takes a considerable amount of code that would detract from the main program (that is my excuse, at any rate).
Critique from Catriona O'Connell <catriona38@hotmail.com>
Here are my thoughts on the Student Code Critique 11 (C Vu 13.4).
There are two major errors in the code as it is printed:
-
A double should use %lf conversion in scanf and printf rather than %f which is meant for float. [actually it is more confusing, %f is correct for printf - there is no %lf in the C Standard - but as you say, it needs to be %lf for scanf. Ed.]
-
There is a missing closing ")" in convergence test.
Had these errors been fixed the code would have worked within the limitations of the Newton-Raphson method. There are however a number of undesirable traits in the code.
-
Infinite loop potential.
-
Fixed convergence criteria.
-
No checks for valid input although scanf does a partial check on the input.
-
No checks for "divide by zero".
-
No prototypes for functions.
-
Lack of comments - even though the code is almost self-commenting.
func(): Returns the value of a function for argument x. The function is
(25/7)(ln(3750*sqrt(abs(x)) - 1/sqrt(abs(x)) - 6
where abs is the absolute value function. The function can be simplified slightly using the properties of logarithms to separate the constant factor. The function is symmetric about the "y-axis" and has a singularity at x = 0
derivative(): Returns the value of the derivative (with respect to x) of func() at x.
The (derivative) function is
(1/(2x))*( (25/7) + 1/(sqrt(abs(x))) )
Note that the factor of x outside the sum correctly allows for the change in sign of the derivative with the sign of x. Note also that the value of the derivative is undefined for x = 0 (that is the limit from both sides does not exist). If the derivative function is called with a value of zero, then it will fail with a divide by zero error.
NewtonRough(): Returns one iteration of the Newton-Raphson algorithm. The choice of name is poor. Something like doNewtonRaphsonIteration() would have been more descriptive.
The Newton-Raphson algorithm is not guaranteed to converge to a steady value. The convergence depends critically on the nature of the function and the initial starting point. In some pathological cases NR may appear to converge but will not converge to a genuine zero of the function. The student should not be surprised by this behaviour, but all too frequently such routines are used without the users being aware of the restrictions. When it works the Newton-Raphson method is quadratically convergent, but when it doesn't work it can rapidly diverge or enter a loop of values. so, the answer to the direct question is that the student's understanding of numerical analysis and algorithms is lacking. The student should be made aware of other root-finding methods to enrich his/her armoury of techniques.
No iterative algorithm should have a potentially infinite loop. If it hasn't converged after a reasonable number of iterations then a new starting point or algorithm needs to be considered. If the number of iterations is set too high then consideration will have to be given to numerical stability and accumulated rounding errors.
There has been much discussion in C Vu in the past about how convergence tests should be done. In this instance the absolute difference in the value of x0 returned by two consecutive iterations is a reasonable measure since graphing the function shows that the zeros lie at around (+/-) 0.005.The maximum number of iterations and the convergence criterion need to be made more visible and stop being magic numbers. The lack of function prototypes forces an order of function definition that is restrictive for future devel
Notes:
More fields may be available via dynamicdata ..