12. Member functions

12.1. Objects and functions

C++ is generally considered and object-oriented programming language, which means that it provides features that support object-oriented programming.

It’s not easy to define object-oriented programming, but we have already seen some features of it:

  1. Programs are made up of a collection of structure definitions and function definitions, where most of the functions operate on specific kinds of structures (or objects).

  2. Each structure definition corresponds to some object or concept in the real world, and the functions that operate on that structure correspond to the ways real-world objects interact.

For example, the Time structure we defined in the More structures chapter obviously corresponds to the way people record the time of day, and the operations we defined correspond to the sorts of things people do with recorded times. Similarly, the Point and Rectangle structures correspond to the mathematical concept of a point and a rectangle.

So far, though, we have not taken advantage of the features C++ provides to support object-oriented programming. Strictly speaking, these features are not necessary. For the most part they provide an alternative syntax for doing things we have already done, but in many cases the alternative syntax is more concise and more accurately conveys the structure of the program.

For example, in the Time program, there is no obvious connection between the structure definition and the function definitions that follow. With some examination, it is apparent that every function takes at least one Time structure as an argument.

This observation is the motivation for member functions. Member functions differ from other functions in two ways:

  1. When we call the function, we invoke in on an object, rather than just call it. People sometimes describe this process as “performing an operation on an object” or “sending a message to an object.”

  2. The function is declared inside the struct definition, in order to make the relationship between the structure and the function explicit.

In the next few sections, we will take the functions from the More structures chapter and transform them into member functions. One thing you should realize is that this transformation is purely mechanical; in other words, you can do it just by following a sequence of steps.

Anything that can be done with a member function can also be done with a nonmember function (sometimes called a free-standing function). But sometimes there is an advantage to one over the other. If you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing.

12.2. print

In the More structures chapter we wrote a fucntion named print_time that operated on Time structures.

struct Time {
    int hour, minute;
    double second;
}

void print_time(const Time& time)
{
    cout << time.hour << ":" << time.minute  << ":" << time.second << endl;
}

To call this function, we had to pass a Time object as an argument.

Time current_time = {9, 14, 30.0};
print_time(current_time);

To make print_time into a member function, the first step is to change the name of the function from print_time to Time::print. The :: operator separates the name of the structure from the name of the function; together they indicate that this is a function named print that can be invoked on a Time structure.

The next step is to eliminate the parameter. Instead of passing an object as an argument, we are going to invoke the function on an object.

As a result, inside the function, we no longer have a paramenter named time. Instead, we have a current object, which is the object the function is invoked on. We can refer to the current object using the C++ keyword this.

One thing that makes life a little difficult is that this is actually a pointer to a structure, rather than a structure itself. A pointer is similar to a reference, but we don’t want to go into the details of using pointers yet. The only pointer operation we need to know for now is the * operator, which converts a structure pointer into a structure. In the following function, we use it to assign the value of this to a local variable named time:

void Time::print()
{
    Time time = *this;
    cout << time.hour << ":" << time.minute << ":" << time.second << endl;
}

The first two lines of this function changed quite a bit as we transformed it into a member function, but notice that the output stream itself did not change at all.

In order to invoke the new version of print, we have to invoke it on a Time object:

Time current_time = {9, 14, 30.0};
current_time.print();

The last step of the transformation process is that we have to declare the new function inside the structure definition:

struct Time {
    int hour, minute;
    double second;

    void print();
}

A function declaration looks just like the first line of the function definition, except that it has a semi-colon at the end. The declaration describes the interface of the function; that is, the number and types of the arguments, and the type of the return value.

When you declare a function, you are making a promise to the compiler that you will, at some point later on in the program, provide a definition for the function. This is sometimes called the implementation of the function, since it contains the details of how the function works. If you omit the definition, or provide a definition that has an interface different from what you promised, the compiler will complain.

12.3. Implicit variable access

Actually, the new version of Time::print is more complicated than it needs to be. We don’t really need to create a local variable in order to refer to the instance variables of the current object.

If the function refers to hour, minute, or second, all by themselves with no dot notation, C++ knows that it must be referring to the current object. So we could have written:

void Time::print()
{
    cout << hour << ":" << minute << ":" << second << endl;
}

This kind of variable access is called “implicit” because the name of the object does not appear explicitly. Features like this are one reason member functions are often more concise than nonmember functions.

12.4. Another example

Let’s convert increment to a member function. Again, we are going to transform on of the parameters into the implicit parameter called this. Then we can go through the function and make all the variable accesses implicit.

void Time::increment(double secs)
{
    second += secs;

    while (second >= 60.0) {
        second -= 60.0;
        minute += 1;
    }
    while (minute >= 60) {
        minute -= 60;
        hour += 1;
    }
}

To declare the function, we can just copy the first line into the structure definition and remove the Time:: in front of it:

struct Time {
    int hour, minute;
    double second;

    void print();
    void increment(double secs);
};

And again, to call it, we have to invoke it on a Time object:

Time current_time = {9, 14, 30.0};
current_time.increment(500.0);
current_time.print();

The output of this program is 9:22:50.

12.5. Yet another example

The original version of convert_to_seconds looked like this:

double convert_to_seconds(const Time& time)
{
    int minutes = time.hour * 60 + time.minute;
    double seconds = minutes * 60 + time.second;

    return seconds;
}

It is straightforward to convert this to a member function:

double Time::convert_to_seconds() const
{
    int minutes = hour * 60 + minute;
    double seconds = minutes * 60 + second;

    return seconds;
}

The interesting thing here is that the implicit parameter should be declared const, since we don’t modify it in this function. But it is not obvious where we should put information about a parameter that doesn’t explicitely exist. The answer, as you can see in this example, is after the parameter list (which is empty in this case).

The print function in the Implicit variable access section should also declare that the implicit parameter is const.

Implicit variable access makes this function easy enough to read that we can even drop the temporary variables without harming readability:

double Time:convert_to_seconds() const
{
    return (hour * 60 + minute) * 60 + second;
}

12.6. A more complicated example

Although the process of transforming functions into member functions is machanical, there are some oddities. For example, after operates on two Time structures, not just one, and we can’t make both of them implicit. Instead, we have to invoke the function on one of them and pass the other as an argument.

Inside the function, we can refer to one of them implicitly, but to access the instance variables of the other we continue to use dot notation.

bool Time::after(const &Time t2) const
{
    if (hour > t2.hour) return true;
    if (hour < t2.hour) return false;

    if (minute > t2.minute) return true;
    if (minute < t2.minute) return false;

    return (second > t2.second);
}

To invoke this function:

if (done_time.after(current_time)) {
    cout << "The bread will be done after it starts." << endl;
}

You can almost read the invocation like English: “If the done-time is after the current time, then…”

12.7. Constructors

Another function we wrote in the More structures chapter was make_time:

Time make_time(double secs)
{
    Time time;
    time.hour = int(secs / 3600.0);
    secs -= time.hour * 3600.0;
    time.minute = int(secs / 60.0);
    secs -= time.minute * 60;
    time.second = secs;

    return time;
}

Of course, for every new type, we need to be able to create new objects of that type. In fact, functions like make_time are so common that there is a special function syntax for them. These functions are called constructors and the syntax looks like this:

Time::Time(double secs)
{
    hour = int(secs / 3600.0);
    secs -= hour * 3600.0;
    minute = int(secs / 60.0);
    secs -= minute * 60;
    second = secs;
}

First, notice that the constructor has the same name as the struct, and no return type. The arguments haven’t changed, though.

Second, notice that we don’t have to create a new time object, and we don’t have to return anything. Both of these steps are handled automatically. We can refer to the new object - the one we are constructing - using the keyword this, or implicitly as shown here. When we write values to hour, minute, and second, the compiler knows we are refering to the instance variables of the new object.

To invoke the constructor, you use syntax that is a cross between a variable declaration and a function call:

Time time(seconds);

This statement declares that the variable time has type Time, and it invokes the constructor we just wrote, passing the value of seconds as an argument. The system allocates space for the new object and the constructor initializes its instance variables. The result is assigned to the variable time.

12.8. Initialize or construct?

Earlier we declared and initialized some Time structures using curly-braces:

Time current_time = {9, 14, 30};
Time bread_time = {3, 35, 0};

Now, using constructors, we have a different way to declare and initialize:

Time time(seconds);

These two functions represent different programming styles, and different points in the history of C++. Maybe for that reason, the C++ compiler requires that you use one or the other, and not both.

If you define a constructor for a structure, then you have to use it to initialize all new structures of that type. The alternative syntax using curly-braces is no longer allowed.

Fortunately, it is legal to overload constructors in the same way we overloaded functions. In other words, there can be more than one constructor with the same “name,” as long as they take different parameters. Then, when we initialize a new object the compiler will try to find a constructor that has matching parameters.

For example, it is common to have a constructor that has a parameter for each instance variable, and that assigns the values of the arguments to the instance variables:

Time::Time(int h, int m, double s)
{
  hour = h; minute = m; second = s;
}

To invoke this constructor, we use the same funny syntax as before, except that the arguments have to be two integers and a double:

Time current_time(9, 14, 30.0);

12.9. One last example

The final example we’ll look at is add_time:

Time add_time(const Time& t1, const Time& t2)
{
    return make_time(convert_to_seconds(t1) + convert_to_seconds(t2));
}

We have to make several changes to make it a member function, including:

  1. Change the name from add_time to Time::add.

  2. Replace the first parameter with an implicit parameter, which should be declared const.

  3. Replace the use of make_time with a constructor invocation.

Here’s the result:

Time Time::add(const Time& t2) const
{
    return Time(convert_to_seconds() + t2.convert_to_seconds());
}

The first time we invoke convert_to_seconds, there is no apparent object! Inside a member function, the compiler assumes that we want to invoke the function on the current object. Thus, the first invocation acts on this; the second invocation acts on t2.

The result of these two function calls are added, and the sum is sent to the version of the Time constructor that takes a single double. Finally the Time object returned by this constructor is returned by the add member function.

12.10. Header files

It might seem like a nuisance to declare functions inside the structure definition and then define the functions later. Any time you change the interface to a function, you have to change it in two places, even if it is a small change like declaring one of the parameters const.

There is a reason for the hassle, though, which is that it is now possible to separate the structure definition and the functions into two files: the header file, which contains the structure definition, and the implementation file, which contains the functions.

Header files usually have the same name as the implementation file, but with the suffix .h instead of .cpp. For the example we have been looking at, The header file is called Time.h, and it contains the following:

struct Time {
    // instance variables
    int hour, minute;
    double second;

    // constructors
    Time(int, int, double);
    Time(double);

    // modifiers
    void increment(double secs);

    // functions
    void print() const;
    bool after(const Time& time2) const;
    Time add(const Time& t2) const;
    double convert_to_seconds() const;
};

Time.cpp contains the definitions of the member functions (we have elided the function bodies to save space):

#include <iostream>
#include "Time.h"
using namespace std;

Time::Time(int h, int m, double s) ...

Time::Time(double secs) ...

void Time::increment(double s) ...

void Time::print() const ...

bool Time::after(const Time& time2) const ...

Time Time::add(const Time& t2) const ...

double Time::convert_to_seconds() const ...

In this case the definitions in Time.cpp appear in the same order as the declarations in Time.h, although it is not necessary.

On the other hand, it is necessary to include the header file using an include statement. That way, while the compiler is reading the function definitions, it knows enough about the structure to check the code and catch errors.

Finally, main.cpp contains the function main along with any functions we want that are not members of the Time structure (in this case their aren’t any):

#include <iostream>
#include "Time.h"
using namespace std;

void main()
{
    Time current_time(9, 14, 30.0);
    current_time.increment(500.0);
    current_time.print();
    cout << endl;

    Time bread_time(3, 35, 0.0);
    Time done_time = current_time.add(bread_time);
    done_time.print();
    cout << endl;

    if (done_time.after(current_time)) {
        cout << "The bread will be done after it starts." << endl;
    }
}

Again, main.cpp has to include the header file.

It may not be obvious why it is useful to break such a small program into three pieces. In fact, most of the advantages come when we are working with larger program:

Reuse:

Once you have written a structure like Time, you might find it useful in more than one program. By separating the definition of Time from main.cpp, you make it easy to include the Time structure in another program.

Managing interactions:

As sytems become large, the number of interactions between components grows and quickly becomes unmanageable. It is often useful to minimize these interactions by separating modules like Time.cpp from the programs that use them.

Separate compilation:

Separate files can be compiled separately and then linked together into a single program later. The details of how to do this depend on your programming environment. As the program gets large, separate compilation can save a lot of time, since you usually need to compile only a few files at a time.

For small programs like the ones in this book, there is no great advantage to splitting up programs. But it is good for you to know about this feature, especially since it explains the statements that appeared in the first program we wrote:

#include <iostream>
using namespace std;

The first line includes the iostream header file that contains the namespace std, which in turn contains declarations for cin and cout and the functions that operate on them. We could have written std::cout every time we wanted to use cout, but the second line, using namespace std;, eliminates the need to do so.

The implementations of the functions cin and cout are stored in a library, sometimes called the standard library, that gets linked to your program automatically. The nice thing is that you don’t have to recompile the library every time you compile a program. For the most part, the library doesn’t change, so there is no reason to recompile it.

12.11. Glossary

constructor(2)

A special function that initializes the instance variables of a newly-created object.

function declaration

A statement that declares the interface to a function without providing the body. Declarations of member functions appear inside structure definitions even if the definitions appear outside.

implementation

The body of a function, or the details of how a function works.

interface

A description of how a function is used, including the number and types of the parameters and the type of the return value.

invoke

To call a function “on” an object, in order to pass the object as an implicit parameter.

12.12. Exercises