Information Hiding
The Time structure from the last lesson did not enforce its invariants. Structures use "naked" variables to represent data, so any part of the program can modify those variables without any validation. Time expects certain relationships between its data members, but cannot enforce those relationships.
In addition, Time is represented in a particular way, as two int data members. Thus code that uses the Time type is tightly coupled to that implementation.
These problems with structured data were noticed in the early 1970s by a Canadian Computer Scientist named David Parnas, who developed a theory of information hiding, and who led the drive towards modular programming with his famous 1971 paper, On the Criteria to be used in Decomposing Systems, written at Carnegie Mellon University.
The Public Interface
The goal of information hiding is to make it possible for clients to use Time objects without ever directly accessing the data members themselves.
To do that, you create a collection of functions, which provides a public interface for your type. These functions are specially dedicated to an individual class, so they are called member functions.
Your interface should be as small as possible, yet comprehensive enough to meet all of your client's needs. What features might those be? (Let's consider H01).
- Arithmetic: calculate the difference and sum of two Time objects
- Input and Output: read into and write out a Time object
Here is your Time structure, including the above interface:
struct Time
{
int hours;
int minutes;
Time sum(const Time& rhs);
Time difference(const Time& rhs);
std::istream& read(std::istream& in);
std::ostream& write(std::ostream& out);
};
Member Functions
Here is your Time structure, including the above interface:
struct Time
{
int hours;
int minutes;
Time sum(const Time& rhs);
Time difference(const Time& rhs);
std::istream& read(std::istream& in);
std::ostream& write(std::ostream& out);
};
The member functions are written as prototypes, inside the structure definition. This is a user-defined type, so you will normally put it inside a header file. Because of that, the library types, istream and ostream are fully qualified.
- The read() and write() functions both take a stream as an argument, so that you can use either the console or a file. Stream arguments must always be passed (and returned) by reference.
- The sum() and difference() member functions will return a new Time object.
Proving the Interface
When designing an interface, its often useful to write a client program, just to try it out. This is called proving your interface. You may find that you need additional member functions. Or, you might find that the prototypes for the functions are not exactly what you need to complete your task, and you can change them at this stage.
Here's the run() function which will act as the client for your new Time type. This is a revision of H01, using member functions.
int run()
{
printHeading(); // already written
Time startTime;
Time duration;
// Prompt and read the input
cout << " Time: ";
if (! startTime.read(cin)) { return die();}
cout << " Duration: ";
if (! duration.read(cin)) { return die();}
// Processing section
Time after = startTime.sum(duration);
Time before = startTime.difference(duration);
// Output section
duration.write(cout) << " hours after, and before, ";
startTime.write(cout) << " is [";
after.write(cout) << ", ";
before.write(cout) << "]" << endl;
return 0;
}
The interface looks OK, so let's go ahead and implement the member functions.
The Implementation File
The header file defines the instance variables used to store the attributes or properties. The implementation file, which typically uses the structure name with a .cpp extension, provides a definition for each member function defined in the interface.
Here's a starter for the implementation file:
#include "time.h"
#include <iostream>
using namespace std;
- #include the header file with the class definition. If you don't, the compiler will flag all of the member functions as errors.
- Surround the header name in "double quotes" not <angle brackets>, which the preprocessor sees as instructions to look for standard library files.
- #include any standard libraries that the implementation uses. Here that is the <iostream> library, which is used for the read() and write() member functions.
Because this is an implementation, not an interface file, you may include a using namespace std;
preprocessor directive.
Implementing Member Functions
To define a member function, specify the name of the function preceded by the name of the structure that it belongs to. To implement write(), for instance, write:
ostream& Time::write(ostream& out)
{
// format and print output here
return out;
}
The name of the member function is Time::write; the double-colon operator (::) is called the scope resolution operator and tells C++ where to look for the function.
You can think of the syntax X::Y as meaning “look inside X for Y.” It is important to use the fully-qualified name of the function when implementing it. The code shown below may compile, but C++ thinks you are implementing a regular (or free) function named write() that has no relationship whatsoever to the Time class.
ostream& write(ostream& out)
{
// Error... not a member function
return out;
}