Lab 5: Functions and Libraries


Introduction

As you may have experienced when purchasing textbooks, books can be quite expensive. One way to save some money would be to share books with someone else. In fact, the more people you can share your books (or CDs, DVDs, etc.) with, the more you can save.

A library (sometimes called a library module or simply a module) is a generalization of this idea of sharing. When a group of people pool their resources, they can buy and share a centralized collection of books. By cooperating this way, everyone has access to a greater set of books than they could afford individually.

In the digital world, sharing libraries is even easier. While a physical book can be actively used by only one person at a time, a digital file can be shared by many people all at the same time. Similarly, in our situation, the code can be written once and shared by many other programs.

In fact, we have already been sharing code written by others when we've #included libraries. For example, in all of the programs thus far, we've used the iostream library. This library defines cout and cin as well as the output (<<) and input (>>) operators. There's actually a lot more in that library (such as the formatting manipulators described in Chapter 4), and we certainly don't want to write all of that code ourselves for every program we write. By reusing the library, we save themselves enormous amounts of time.

As we'll see in this lab, there's nothing magical about a library, In fact, you will create your own library. The main difference between the libraries you write and the standard libraries of C++ is their size. The libraries provided in C++ are much larger than the ones you will write.

We'll begin writing our own library in this lab — an English-to-metric conversion library. It will contain only two basic conversion functions, but there are several others that could be added. For example, see the project described here. Your instructor may have you extend this library to include some of them.

Review

We begin with a brief review of what we know about function libraries. We've seen that C++ provides a variety of function libraries, including iostream that provides input and ouput, cmath that provides various mathematical functions, and cctype that provides character-processing functions.

To use a library function, a program must use the #include directive to make the contents of that program available to the program; for example,

    #include <cmath>
inserts basic contents of the library into your program so they can be used in your program.

In most programs, we are interested in functions such as sqrt() and pow() and operators such as << and >>. (Note: To indicate that an identifier such as pow is the name of a function as opposed to naming a variable, constant, file, etc., it is common to attach parentheses to its name.) To call a function you must specify its name and a list of arguments — values to be sent to the function (or as we'll see later, possibly returned by it) — in parentheses. For example, if we have numeric variables x and y, we can calculate xy — that is, x to the yth power — by calling the function pow() in the cmath library as follows:

pow(x, y)

Question #5.1: In the function call above, what is the name of the function being called? What are the arguments in the function call?

The code associated with the function is executed, and the result xy is returned. Since this is an expression, you will undoubtedly put it into a context; for example, assign it to a variable

double result = pow(x, y);
or output it:
cout << "The answer is " << pow(x, y) <<".\n";

Planning a Library

The library we will create in this lab exercise will provide a set of functions to convert English-system measurements to their metric-system counterparts. We will name our library metric.

The first thing we must decide is what measurement conversions we wish our library to provide. Here are some candidates:

English Unit Metric Unit Conversion Equation
Inches Centimeters 1 inch = 2.54 cm
Feet Centimeters 1 foot = 30.48 cm
Feet Meters 1 foot = 0.3048 m
Yards Meters 1 yard = 0.9144 m
Miles Kilometers 1 mile = 1.609344 km
Ounces Grams 1 ounce = 28.349523 g
Pounds Kilograms 1 pound = 0.453592 kg
Tons Kilograms 1 ton = 907.18474 kg
Pints Liters 1 pint = 0.473163 l
Quarts Liters 1 quart = 0.946326 l
Gallons Liters 1 gallon = 3.785306 l

In this lab exercise, you will write two functions, one to convert feet into meters and the other to convert meters into feet. (You instructor may assign more of the conversions above to add to the library.) We will put hese functions in our metric library so they can be shared by any and all programs that need to convert between feet and meters and thereby avoid having to "reinvent the wheel."

When you begin work with the the four files described in the Files section below, you will see that you've already been provided with the feet-to-meters conversion. We'll walk through this example, and then it will be your task to add the meters-to-feet conversion.

Library Structure

A library consists of three separate files:

The documentation file is not strictly necessary, any more than are comments within a program; however it is good to provide such a file, and store within it all of the information needed to use the library. In this our first library, we will place in it the usual documentation we have been using for functions — what it does, what it receives, what it returns.

The header file is necessary, and its role is to provide an interface to the library, by providing just enough information for a program to use the functions in the library, without specifying their details. In contrast, the role of the implementation file is to provide complete definitions of the library's functions. You'll notice some redundancy in these two files, but this is unavoidable.

Helpful hint: Remember to check both your header file and implementation file when something goes wrong with your library.

Why all of these files? The reason has to do with program maintenance. If we are writing a library, then we expect that many programs will make use of it. It is often the case that even a well-designed library may need to be updated if a better way is discovered to perform one of its functions or even if a function needs to be fixed. If we have designed our functions carefully, then updating a library function should simply involve altering its definition which is in the implementation file. We may be able to leave the header file alone and thereby save users of the library from having to recompile their programs.

Files

In this lab you will be using four files:

Where each of these files is to be stored may vary with the programming environment you are using. For example, if you are using a project-based programming environment such as Visual C++, you may create a project and add:

If you are using some other C++ environment, you will probably create a directory to store these four files.

Header File Structure

Edit metric.h, the header file of our library. For convenience, we base the name of this file on the name of our library. It is common practice to attach the extension .h to the end of the filename to specify that this is the header file.

Personalize the opening documentation of this file as your instructor has specified.

A simplified general pattern for a library header file is as follows:

Opening Documentation
List of Items Provided

The opening documentation is a comment describing the purpose of the library, who wrote it (and other information your instructor specifies). For the list of items provided by the library, for simplicity, we will use a list of the prototypes of the functions that the library provides.

Function Design

The functions in the library should be designed in the same way as all functions. Thus, we will use OCD to design our library functions.

Behavior

First, we describe the behavior of our feet-to-meters function:

Our function should receive from its caller the number of feet to be converted, and should check that this value is non-negative. If it is, it should convert that quantity to meters by multiplying it by 0.3048, and then return the resulting value to the caller.

Note that a function typically receives values from whoever called the function whereas a program typically inputs values from the keyboard. Similarly, a function typically returns a value to whoever called the function whereas a program typically outputs values to the screen. This distinction is important because most functions do not use cin or cout. We won't need them in these library functions.

We should also try to anticipate what could go wrong. The function will only produce a correct result if the value it receives is non-negative; this is a precondition since it is a condition that must be true before the function can be executed.

Now let's consider our other function, converting meters to feet. This function won't be much different than the feet-to-meters function. So, the basic structure for the behavior should be the same. The values it receives and returns will be different and the computation is different (division instead of multiplication), but they are still quite similar.

Question #5.2: Write the behavior paragraph for the meters-to-feet function.

Objects

We list the objects of a function in the same way that we do for a program, except we also want to describe their movement. For each object, does it move into the function from outside, does it move from the function out to the outside, or is it purely local to the function?

Description Type Kind Movement Name
the number of feet double varying in feet
the conversion factor double constant local 0.3048
the corresponding number of meters double varying out meters

The movement column of this chart summarizes what data is coming in, what is going out, and what is simply being used inside the function. This allows us to write our specification, which also includes our precondition:

Specification:
receive: feet, the number of feet to be converted.
precondition: feet is not negative.
return: meters, the equivalent of feet in meters.

Keep in mind that formulating this information for a function is done in basically the same manner as for a program. The behavior paragraph is used to create the object chart. The nouns in the behavior are our objects, and the behavior explains the type, kind, and movement of the objects. The behavior even suggests the names of our objects.

Once we have the object chart, we can formulate the specification, looking for the objects moving into and out of the function. The behavior paragraph establishes our preconditions.

Question #5.3: Using your behavior description, make an object chart for the meters-to-feet function.

Question #5.4: Using your behavior description and object chart, formulate a specification for the meters-to-feet function.

The specification of a function provides us all we need to write a prototype, which is how we translate the specification for our function into C++ code. It specifies what the function does, not how is does it. We will need a prototype for every function we write.

The general pattern for a function prototype is

returnType functionName(parameterList);
where

Each value that our function receives must be declared as a parameter. A parameter is basically just a variable like the ones we've declared and used up to now. The differences are subtle:

Because parameters are like variables, we will use the same naming convention as we've been using for variables: their names begin with a lowercase letter, with every word after the first capitalized. The same convention is used for naming functions. Athough these are are not universal conventions, they are commonly used and are the ones that we will use.

Using our specification and with some imagination, we can come up with the components of our prototype:

Now we can assemble these components into our prototype:
    double feetToMeters(double feet);

You will see this prototype in both the header file (metric.h) and the documentation file (metric.txt). C++ requires the prototype in the header file so that our program (and others) can see it. We document it in the documentation file so that users of the library know how to use our function; that's why you'll see the specification just above it.

Question #5.5: Write a prototype for the meters-to-feet function. Add your prototype to both the header file and the documentation file. Also, add a comment containing the specification for the prototype in the documentation file.

As this illustrates, the stages of software design are fluid, not firm. We can build a function's prototype (a part of coding) during our design stage, as soon as we have enough information.

Operations

Continuing with our design, our list of needed operations is quite short:

Description Predefined? Name Library
get a value from the caller yes feet built-in
check that the value is not negative yes >= 0 built-in
halt the program if the value is negative yes assert() cassert
multiply two real values yes * built-in
return a value to the caller yes return built-in

We've already seen the parameter mechanism that allows us to get the feet value. We'll see more when we write our function definition. We'll also see the return mechanism in action. The key now is that all of these operations are built into C++. The relational operators and the assert() function can be used to handle the preconditions.

Question #5.6: Construct a similar operation chart for the meters-to-feet function.

Algorithm

We can then organize these operations and objects into the following algorithm:

  1. Receive feet from the caller.
  2. Check if feet is non-negative; halt the program if it isn't.
  3. Compute meters = feet * 0.3048.
  4. Return meters.
Our behavior description and the operations lead directly to this algorithm.

Question #5.7: Write a similar algorithm for the meters-to-feet function.

Implementation File Structure

We store declarations in the header file. That's all a function prototype is: a declaration. It declares the function's name, it's parameters, and return type. But we haven't written any code for the function to execute!

That's the purpose of the implementation file. It contains the definitions of our library functions; that is, it contains the code for these functions. Implementation files are usually named the same as our header file, except that they end in .cpp.

The general pattern for an implementation file is as follows:

OpeningDocumentation
Include directives
DefinitionList

If you haven't already done so, personalize the opening documentation in metric.cpp.

A library's header file should normally be #included in the implementation file. For this, we use a variation of the #include directive. We've used the form

#include <iostream>
for system libraries. A personal library that we have in our working directory uses different punctuation:
#include "metric.h"
Add this line to your implementation file.

Defining A Function

The general pattern for a function definition is

returnType functionName(parameterList)
{
  statementList
}
where The first bullet point here is very useful. Once we've written our prototype, we can copy this line to the implementation file for the beginning of the function definition, but we must be sure to remove the semicolon at the end. A prototype requires that punctuation, but it will cause an error in a function definition.

A function stub is a minimal function definition. For example, this is the function stub for feetToMeters():

double feetToMeters(double feet)
{
}
Why bother with a function stub? Because we can compile the program with a function stub so we can check our program to make sure that at least what we've written so far contains no syntax errors.

Of course, we eventually fill the stub with code so that the function does something. That's already been done for feetToMeters(). Now it's up to you to do this for the meters-to-feet function.

Start by writing a function stub for the meters-to-feet function, and add it to the implementation file. Compile the program. You should be able to compile the program without errors. You may get a warning that your function does not return anything, but we'll let that slide for now . Also, you only have to compile the metric.cpp file itself; you do not have to link it with the driver yet. You can, but you may get errors that you should ignore.

Now we can translate our algorithm into code, as we'll illustrate using out feet-to-meters conversion.

1. Receive feet from the caller.

This step is taken care of for us by the C++ function-call mechanism. We've already done it! Declaring a parameter in our function heading

    double feetToMeters(double feet)

informs C++ that we want to receive this value. That's the whole purpose of parameters!

Nevertheless, it's a good idea to put this step in our algorithm to remind us where the data comes from; but once we have the parameters declared, the function has already received the values from whatever called it.

2. Check if feet is non-negative and halt the program if it isn't.

This is our precondition. We decided that we'd do this with the assert() statement:

    assert(feet >= 0);
That's exactly what's been done in our definition of feetToMeters().

3. Compute meters = feet * 0.3048.

This step has been implemented in our declaration of meters:

    double meters = feet * 0.3048;
Remember that we have to declare all of our variables. The feet variable has already been declared as a parameter---that counts! However, this is the first time we've used meters, so we declare it and initialize it with our computation.

4. Return meters.

The third operation can be performed using the C++ return statement. Its general pattern is:

return expression;
where expression is any valid C++ expression. The result of the expression is sent back to whatever code called this function.

Now it's your turn. Use your algorithm for the meters-to-feet function plus the feetToMeters() example to implement your algorithm in the stub that you wrote earlier. Compile the code as you go along (probably after you write each line of code); be sure it compiles without errors or warnings when you finish.

Writing a Program to Test the Library

Next, open driver.cpp and personalize its documentation.

The program in this file was written to test the function we've written. Such programs are called driver programs because all they do is "test drive" the functions in a library.

Designing The Driver Program

Most driver programs use the same algorithm:

  1. Display a prompt for whatever values the function requires as arguments.
  2. Read those values from cin.
  3. Call the function that you're testing using the input values as arguments.
  4. Display via cout the result of calling the function plus a descriptive label.
Since every driver program uses this same basic algorithm, we're going to go right on to the coding stage of our development.

Coding the Driver Program

You've been given the code for part of the driver for this lab — the part that tests the feetToMeters() function. You'll have to add another part to test-drive your metersToFeet() function. Also, you will have to write the drivers for future labs from scratch so read over this code carefully and make sure that you understand what it does.

Step 1 of the algorithm is merely an output statement. You've written a few of these already.

Step 2 is a matter of reading in data. This take a little more thought: you need to know what objects you need to input and their data types. Declare variables for these objects, and then read in the data.

Step 3 will usually consist of a variable declaration with an initialization that calls the function we're testing.

Step 4 is just an output statement that displays a label describing the result and displays the result itself.

Now, before we can use a library, we must include its header file in the driver file. This has been done already:

#include "metric.h"
Notice the saving we're already getting. We write the header file once, but we're using it twice. And this is just in one program! Be sure to use the #include "..." form to include a header file from your personal library.

Calling a Function

Let's look more closely at Step 3 of our driver algorithm.

At the beginning of this lab, we quickly looked at the pow() function. If we had two variables x and y, we can call the function with the expression pow(x,y) and then use the value that this returns.

But we got the pow() function from a system library. What do we do with a function that we write ourselves? Exactly the same thing.

Consider feetToMeters(). It receives a number of feet. If we want to convert 1.0 foot to meters, we could write this:

feetToMeters(1.0)
(For now, don't worry about the context — that is, what we do with the result). If we want to convert 2.0 feet to meters, we could write
feetToMeters(2.0)
If we want to convert x raised to the yth power feet to meters, we could write
feetToMeters(pow(x,y))
In the case of our driver, however, the driver reads in a value, places it in feet, and we want to convert that value:
feetToMeters(feet)
The driver uses this as the expression to compute the value of the meters variable.

Our approach to the function has changed. When we declared the function, we declared a parameter — a variable — to receive the number of feet. But now that we're calling the function, we can pass in a literal, a variable, or any double expression. A value that we pass into a function is known as an argument.

Helpful hint: A parameter is always a variable, it is part of a function definition, and we get to choose the name. An argument can be any expression, and it is part of a function call.

There are three important rules to remember when calling a function:

  1. The number of arguments in a function call must equal the number of parameters in the function's prototype, or a compilation error will occur.
  2. The number of arguments in a function call must equal the number of parameters in the function's definition, or a linking error will occur.
  3. The types of the first argument and first parameter, the second argument and second parameter, the third argument and third parameter, etc. must be the same, or a compilation error will occur.
If you get any complaints about your function, make sure you check the function call, the function prototype, and the function definition.

Compile and link all of your code, and test your program.

As it stands, however, the program only tests feetToMeters(). It should also test metersToFeet(). There are several ways to do this:

Let's try the third option here. So instead of reading in a number of feet, let's read in a generic number. Let's call it measurement. We can then use this one value as an argument to both functions. Its meaning in these functions will be different, a number of feet in the one and a number of meters in the other, but that's fine. Here's what the code would look like to test feetToMeters():

// Step 2.
double measurement;
cin >> measurement;
// Step 3.
double meters = feetToMeters(measurement);
// Step 4.
cout << measurement << " feet == " << meters << " meters\n";

Now all you have to do is use this code in the driver, and then duplicate Steps 3 and 4 for the meters-to-feet function.

Testing

Once the program is successfully translated, we are ready to test what we have written, to see if we can discover any logic errors. Execute the program, and use the following sample data to test the correctness offeetToMeters():

Feet Meters
1.0 0.3048
3.3 1.00584
−5.9 ERROR

Since feetToMeters() is a linear function, two test values are sufficient to verify its correctness. Be sure to note and fix any discrepancies.

The third test value tests our precondition. This is also a good idea to make sure that the function fails when it's supposed to.

Question #5.8: What error message does the program give when you enter the invalid input?

Since you've been given feetToMeters(), it should run correctly. Your meters-to-feet function may not. The sample data above can also be used to test your meters-to-feet function.

Question #5.9: What sample data are you going to use and what results do youexpect for your meters-to-feet function?

Debug and modify your program until you get the correct results from both functions.

Maintenance

Better Error Messages

After a library is released, the developers may start getting complaints that the error message the functions give when they fail are far too cryptic. The error message produced by assert() is one such example. On some compilers, the message may be quite difficult (and perhaps impossible for some people) to understand. Alternatively, we might code our own error handling.

Consider, for example, feetToMeters(). We formulated its precondition almost at the very beginning and we wrote an expression for it: feet >= 0. We chose to use assert() as a "quick and dirty" way to test this precondition.

But this isn't the only alternative. We would almost surely prefer more "user-friendly" error messages, especially if our library is to be used by other people. Think about what we want to happen: if the precondition is false (i.e., feet is negative), then we want to display a helpful error message on the screen and stop the program.

For this, it is best to display the error messages using a new object for output: cerr. Although cerr is of type ostream and is used in the same way as cout to produce output to the screen, cout should be used for normal output while cerr should be used for error messages. (Why? See the footnote at the end of this lab exercise.)

Four operations are needed here:

Description Predefined? Name Library
feet is negative yes < built-in
if...then... yes if statement built-in
display error message yes << iostream
stop the program yes exit() cstdlib

The exit() function, when executed, will stop the program immediately. The function takes one argument, an int. The actual value of the argument is really up to you; convention is that any non-zero value means the program stopped because of a problem. So we may as well keep it simple and use -1 or 1.

Using this information, we can replace Step 2 of the algorithm for feetToMeters() with:

  1. If feet is negative, then...
    1. Display a helpful error message.
    2. Exit the program.

So, in the code, we can replace the call to assert() with this code:

if (feet < 0)
{
  cerr << "The number of feet is negative in feetToMeters()." << endl;
  exit (-1);
}
This is much more informative that a cryptic computer-generated message.

Make this change to feetToMeters(). Compile and test your program, especially the failure test.

Once you have this working for feetToMeters(), make similar changes to your meters-to-feet function. The new behavior, objects, operations, and algorithm should parallel the new design for feetToMeters() as should the new code.

Repeated Testing

It's a bit of a nuisance to have to execute the driver program three times for the three test values. Also, testing some libraries may require a much larger set of test values. Thus, it is usually best to build a repetition into driver programs to faciliate testing.

There are two kinds of repetition statements that we have studied thus far:

Use one of these in your driver program to allow repeated testing of your library without re-executing it for each test value.

[Footnote: cout sends its output to a buffer and from there to the screeen only when directed to do so or the buffer becomes full; but cerr sends its output directly to the screen. If a program crashes sometime after an output statement has been executed, an error message from cerr will have been displayed on the screen, whereas one from cout might still be in the buffer and never make it to the screen.]


Lab Home Page


Report errors to Larry Nyhoff (nyhl@cs.calvin.edu)