Organizing programs and data
4.1 Organizing computations
4.1.5 Using functions to calculate a student's grade
The whole point of writing these functions is to use them in solving problems. For example, we can use them to reimplement our grading program from §3.2.2/46:
// include directives and using-declarations for library facilities // code for median function from §4.1.1/53
// code for grade(double, double, double) function from §4.1/52
// code for grade(double, double, const vector<double>&) function from §4.1.2/54 // code for read_hw(istream&, vector<double>&) function from §4.1.3/57
int main() {
// ask for and read the student's name cout << "Please enter your first name: ";
string name;
cin >> name;
cout << "Hello, " << name << "!" << endl;
// ask for and read the midterm and final grades
cout << "Please enter your midterm and final exam grades: ";
double midterm, final;
cin >> midterm >> final;
// ask for the homework grades
cout << "Enter all your homework grades, "
"followed by end-of-file: ";
vector<double> homework;
// read the homework grades read_hw(cin, homework);
// compute and generate the final grade, if possible try {
double final_grade = grade(midterm, final, homework);
streamsize prec = cout.precision();
cout << "Your final grade is " << setprecision(3) << final_grade << setprecision(prec) << endl;
} catch (domain_error) {
cout << endl <<"You must enter your grades. "
"Please try again." << endl;
return 1;
} return 0;
}
The changes from the earlier version are in how we read the homework grades, and in how we calculate and write the result.
After asking for our user's homework grades, we call our read_hw function to read the data. The while statement inside read_hw repeatedly reads homework grades until we hit end-of-file or
encounter a data value that is not valid as a double.
The most important new idea in this example is the try statement. It tries to execute the statements in the { } that follow the try keyword. If a domain_error exception occurs anywhere in these
statements, then it stops executing them and continues with the other set of { } -enclosed
statements. These statements are part of a catch clause, which begins with the word catch, and indicates the type of exception it is catching.
If the statements between try and catch complete without throwing an exception, then the program skips the catch clause entirely and continues with the next statement, which is return 0; in this example.
Whenever we write a try statement, we must think carefully about side effects and when they occur.
We must assume that anything between try and catch might throw an exception. If it does so, then any computation that would have been executed after the exception is skipped. What is important to realize is that a computation that might have followed an exception in time does not necessarily follow it in the program text.
For example, suppose that we had written the output block more succinctly as // this example doesn't work
try {
streamsize prec = cout.precision();
cout << "Your final grade is " << setprecision(3)
<< grade(midterm, final, homework) << setprecision(prec);
} ...
The problem with this rewrite is that although the implementation is required to execute the <<
operators from left to right, it is not required to evaluate the operands in any specific order. In particular, it might call grade after it writes Your final grade is. If grade throws an exception, then the output might contain that spurious phrase. Moreover, the first call to setprecision might set the output stream's precision to 3 without giving the second call the opportunity to reset the precision to its previous value. Alternatively, the implementation might call grade before writing any output;
whether it does so depends entirely on the implementation.
This analysis explains why we separated the output block into two statements: The first statement ensures that the call to grade happens before any output is generated.
A good rule of thumb is to avoid more than one side effect in a single statement. Throwing an exception is a side effect, so a statement that might throw an exception should not cause any other side effects, particularly including input and output.
Of course, we cannot run our main function as written. We need the include directives and
using-declarations for the library facilities that the program uses. We also use the names read_hw and the grade function that takes a const vector<double>& third argument. The definitions of these functions, in turn, use the median function and the grade function that takes three doubles.
In order to execute this program, we have to ensure that those functions are defined (in the proper order) before our main function. Doing so yields an inconveniently large program. Rather than write it out directly here, we'll see in §4.3/65 how we can partition such programs more succinctly into files. Before we do so, let's look at better ways to structure our data.
4.2 Organizing data
Computing one student's grades may be useful to that student, but the computation is simple enough that a pocket calculator could handle it almost as well as our program. On the other hand, if we are teaching a course, we will want to compute grades for a class full of students.
Let's revise our program to make it useful for an instructor.
Instead of interactively reporting one student's grade, we'll assume that we are given a file that contains many students' names and grades. Each name is followed by a midterm grade and a final exam grade, and then by one or more homework assignment grades. Such a file might look like
Smith 93 91 47 90 92 73 100 87 Carpenter 75 90 87 92 93 60 0 98 ...
Our program should calculate each student's overall grade using medians: The median
homework grade counts 40%; the final, 40%; and the midterm, 20%. For this input, the output would be
Carpenter 86.8 Smith 90.4
In the output, we want the report to be organized alphabetically by student, and we want the final grades to line up vertically so that they are easier to read. These requirements imply that we'll need a place to store the records for all the students, so that we can alphabetize them.
We'll also need to find the length of the longest name, so that we know how many spaces to put between each name and its corresponding grade.
Assuming that we have a place to store the data about a single student, we can use a vector to hold all the student data. Once the vector contains data for all the students, we can sort it, and then calculate and write each student's grades. We'll start by creating a data structure to hold the student data, and by writing some auxiliary functions to read and process those data.
After we have developed these abstractions, we'll use them to solve the overall problem.