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
Organizing programs and data
Although the program in §3.2.2/46 is larger than we would like, it would have been larger still without vector, string, and sort. These library facilities, like others that we have used, share several qualities: Each one
Solves a particular kind of problem
Is independent of most of the others
Has a name
Our own programs have the first of these qualities, but lack the others. This lack is fine for small programs, but as we set out to solve larger problems, we will find that our solutions will become unmanageable unless we break them into independent, named parts.
Like most programming languages, C++ offers two fundamental ways of organizing large programs: functions (sometimes called subroutines) and data structures. In addition, C++ lets programmers combine functions and data structures into a single notion called a class, which we'll explore starting in Chapter 9.
Once we have learned how to use functions and data structures to organize our computations, we also need the ability to divide our programs into files that we can compile separately and combine after compilation. The last part of this chapter will show how C++ supports separate compilation.
4.1 Organizing computations
We shall begin by writing a function to calculate a student's final grade from the midterm and final exam grades and overall homework grade. We'll assume that we've already calculated the overall homework grade from the individual homework grades, which we have been computing as the average or median. Aside from that assumption, this function will use the same policy as the one that we've been using all along: Homework and the final exam contribute 40% each to the total, and the midterm makes up the remaining 20%.
Whenever we do—or might do—a computation in several places, we should think about putting it in a function. An obvious reason for doing so is that then we can use the function instead of redoing the computation explicitly. Not only does using functions reduce our total programming effort, but doing so also makes it easier for us to change the
computation if we wish. For example, assume we wanted to change our grading policy. If we had to hunt through every program we had ever written, looking for the parts that dealt with grading, we would probably become discouraged quickly.
There is a more subtle advantage to using functions for such computations: A function has a name. If we name a computation, we can think about it more abstractly—we can think more about what it does and less about how it works. If we can identify important parts of our problems, and create named pieces of our programs that correspond to those parts, then our programs will be easier to understand and the problems easier to solve.
Here is a function that computes grades according to our policy:
// compute a student's overall grade from midterm and final exam grades and homework
grade
double grade(double midterm, double final, double homework) {
return 0.2 * midterm + 0.4 * final + 0.4 * homework; }
Until now, all the functions that we've defined have been named main. We define most other functions similarly, by specifying the return type, followed first by the function name, next by a parameter list enclosed in ( ), and, finally, by the function body, which is enclosed in { }. The rules are more complicated for functions that return values that denote other functions; see §A.1.2/297 for the full story.
In this example, the parameters are midterm, final, and homework, each of which has type double. They behave like variables that are local to the function, which means that calling the function creates them and returning from the function destroys them.
As with any other variables, we must define the parameters before using them. Unlike other variables, defining them does not create them immediately; only calling the function creates them. Therefore, whenever we call the function, we must supply corresponding arguments, which are used to initialize the parameters when the function begins
execution. For example, in §3.1/36 we computed a grade by writing
cout << "Your final grade is " << setprecision(3) << 0.2 * midterm + 0.4 * final + 0.4 * sum / count << setprecision(prec) << endl;
If we had the grade function available, we could have written
cout << "Your final grade is " << setprecision(3) << grade(midterm, final, sum / count)
<< setprecision(prec) << endl;
Not only must we supply arguments that correspond to the parameters of the functions that we call, but we must supply them in the same order. Accordingly, when we call the grade function, the first argument must be the midterm grade, the second must be the final exam grade, and the third must be the homework grade.
Arguments can be expressions, such as sum / count, not just variables. In general, each argument is used to initialize the. corresponding parameter, after which the parameters behave like ordinary local variables inside the function. So, for example, when we call grade(midterm, final, sum / count), the grade function's parameters are initialized to copies of the arguments' values, and do not refer directly to the arguments themselves. This behavior is often called call by
value, because the parameter takes on a copy of the value of the argument.
4.1.1 Finding medians
Another problem that we solved in §3.2.2/46, and that we can imagine wanting to solve in other contexts, is finding the median of a vector. We'll see in §8.1.1/140 how to define a function that is so general that it works with a vector of any type of value. For now, we'll limit our attention to vector<double>.
To write our function, we'll start with the part of the program in §3.2.2/47 that computes medians, and make a few changes:
// compute the median of a vector<double>
// note that calling this function copies the entire argument vector double median(vector<double> vec)
{
typedef vector<double>::size_type vec_sz; vec_sz size = vec.size();
if (size == 0)
throw domain_error("median of an empty vector"); sort(vec.begin(), vec.end());
return size % 2 == 0 ? (vec[mid] + vec[mid-1]) / 2 : vec[mid]; }
One change is that we named our vector vec, rather than homework. After all, our function can compute the median of anything, not just homework grades. We also eliminated the variable median, because we no longer need it: We can return the median as soon as we've calculated it. We are still using size and mid as variables, but now they are local to the median function and, therefore, inaccessible (and irrelevant) elsewhere. Calling the median function will create these variables, and returning from the function will destroy them. We define vec_sz as a local type name, because we don't want to conflict with anyone who might want to use that name for another purpose.
The most significant change is what we do if the vector is empty. In §3.2.2/46, we knew that we would have to complain to whoever was running our program, and we also knew who that person would be and what kind of complaint would make sense. In this revised version, on the other hand, we don't know who is going to be using it, or for what purpose, so we need a more general way of complaining. That more general way is to throw an exception if the vector is empty.
When a program throws an exception, execution stops in the part of the program in which the throw appears, and passes to another part of the program, along with an exception object, which contains information that the caller can use to act on the exception.
The most important part of the information that is passed is usually the mere fact that an exception was thrown. That fact, along with the type of the exception object, is usually enough to let the caller figure out what to do. In this particular example, the exception that we throw is domain_error, which is a type that the standard library defines in header <stdexcept> for use in reporting that a function's argument is outside the set of values that the function can accept. When we create a domain_error object to throw, we can give it a string that describes what went wrong. The program that catches the exception can use this string in a diagnostic message, as we shall see in §4.2.3/65.
There is one more detail about how functions behave that is important to understand. When we call a function, we can think of the parameters as local variables whose initial values are the arguments. If we do so, we can see that calling a function involves copying the arguments into the parameters. In particular, when we call median, the vector that we use as an argument will be copied into vec.
In the case of the median function, it is useful to copy the argument to the parameter, even if doing so takes significant time, because the median function changes the value of its parameter by calling sort. Copying the argument prevents changes made by sort from propagating back to the caller. This behavior makes sense, because taking the median of a vector should not change the vector itself.
4.1.2 Reimplementing our grading policy
The grade function in §4.1/52 assumes that we already know the student's overall homework grade, and not just the individual homework assignments' grades. How we obtain that grade is part of our policy: We used the average in §3.1/36 and the median in §3.2.2/47. Accordingly, we might wish to express this part of our grading policy in a function, along the same lines as we did in §4.1/52:
// compute a student's overall grade from midterm and final exam grades // and vector of homework grades.
// this function does not copy its argument, because median does so for us. double grade(double midterm, double final, const vector<double>& hw)
{
if (hw.size() == 0)
throw domain_error("student has done no homework"); return grade(midterm, final, median(hw));
This function has three points of particular interest.
The first point is the type, const vector<double>&, that we specify for the third argument. This type is often called "reference to vector of const double." Saying that a name is a reference to an object says that the name is another name for the object. So, for example, if we write
vector<double> homework;
vector<double>& hw = homework; // hw is a synonym for homework
we are saying that hw is another name for homework. From that point on, anything we do to hw is equivalent to doing the same thing to homework, and vice versa. Adding a const, as in
// chw is a read-only synonym for homework const vector<double>& chw = homework;
still says that chw is another name for homework, but the const promises that we will not do anything to chw that might change its value.
Because a reference is another name for the original, there is no such thing as a reference to a reference. Defining a reference to a reference has the same effect as defining a reference to the original object. For example, if we write
// hw1 and chw1 are synonyms for homework; chw1 is read-only vector<double>& hw1 = hw;
const vector<double>& chw1 = chw;
then hw1 is another name for homework, just as hw is, and chw1, like chw, is another name for homework that does not allow write access.
If we define a nonconst reference—a reference that allows writing—we cannot make it refer to a const object or reference, because doing so would require permission that the const denies. Therefore, we cannot write
vector<double>& hw2 = chw; // error: requests write access to chw
because we promised not to modify chw.
Analogously, when we say that a parameter has type const vector<double>&, we are asking the implementation to give us direct access to the associated argument, without copying it, and also promising that we won't change the parameter's value (which would otherwise change the argument too). Because the parameter is a reference to const, we can call this function on behalf of both const and nonconst vectors. Because the parameter is a reference, we avoid the overhead of copying the argument.
The second point of particular interest about the grade function is that like the one in §4.1/52, it is named grade—even though it calls the other grade function. The notion that we can have several functions with the same name is called overloading, and figures prominently in many C++ programs. Even though we have two functions with the same name, there is no ambiguity, because whenever we call grade, we will supply an argument list, and the implementation will be able to tell from the type of the third argument which grade function we mean.
The third point is that we check whether homework.size() is zero, even though we know that median will do so for us. The reason is that if median discovers that we are asking for the median of an empty vector, it throws an exception that includes the message median of an empty vector. This message is not directly useful to someone who is
computing student grades. Therefore, we throw our own exception, which we hope will give the user more of a clue as to what has gone wrong.
4.1.3 Reading homework grades
Another problem that we have had to solve in several contexts is reading homework grades into a vector.
There is a problem in designing the behavior of such a function: It needs to return two values at once. One value is, of course, the homework grades that it read. The other is an indication of whether the attempted input was successful. There is no direct way to return more than one value from a function. One indirect way to do so is to give the function a parameter that is a reference to an object in which it is to place one of its results. This strategy is common for
functions that read input, so we'll use it. Doing so will make our function look like this: