Operator Functions and Overloading

In C++, all operators can be written as functions.  For example, the classic "Hello World" program uses several insertion operators with cout.  In fact, each insertion operator can be rewritten as a function call.

Listing 1. Hello World calling Operator Functions

#include <iostream.h>

int
main()
{
	// use the standard forms
     cout << "Hello, Cruel World!  " << "Today I am " << (1 + 1)
	  << " years old." << ' ' << ' ';
     cout <<  "I weigh " << 80.5 << " kilograms." << endl;

	// actually call the operator functions
     cout.operator<<("Hello, Cruel World!  "); 
     cout.operator<<("Today I am ");
     cout.operator<<(2);
     cout.operator<<(" years old.");
     cout.operator<<(' ');
     cout.operator<<(' ');
     cout.operator<<("I weigh ");
     cout.operator<<(80.5);
     cout.operator<<(" kilograms.");
     cout.operator<<('\n');

     return 0;
}

Download this program

Four distinct insertion operator functions are being called here:  one for strings, one for single characters, one for integers, and one for floating-point numbers.  In iostream.h the function declarations for these look something like the following:

ostream & operator<<( ostream &, const char * );
ostream & operator<<( ostream &, char );
ostream & operator<<( ostream &, int );
ostream & operator<<( ostream &, double );

Notice that each operator function's name is operator<<.  The iostream library takes advantage of C++'s overloading feature that lets you have two or more functions with the same name, as long as they take different types and/or different numbers of arguments.  In the case of insertion operators, each much take two parameters only, the first parameter must be a reference to an ostream, and therefore, the second parameter's type must be unique.  Otherwise, the compiler would give errors.

Defining Our Own Insertion Operator

Consider the program below.  We can define a data structure, called point, that models a Cartesian point.  Since this means that we are adding a new data type to the language, we can define an insertion operator function that will handle the new type.

Listing 2.  User-defined Data Structure and Insertion Operator.

#include <iostream.h>

struct point
{
     int x;
     int y;
};

ostream & operator<<(ostream &, const point &);

int
main()
{
     point p;
     p.x = 1;
     p.y = 2;
     cout << "Point is " << p << endl;
     return 0;
}

ostream & 
operator<<(ostream & theStream, const point & thePoint)
{
     theStream << '(' << thePoint.x << ',' << thePoint.y << ')';
     return theStream;
}

Download this program

Notice that the insertion operator function for our point structure simply calls the "built-in" insertion operators for the individual structure members -- each of which are simple types. The compiler figures out which insertion operator function to call -- this is another feature of C++ called polymorphism.  Each data type instance can be "inserted", but each is inserted in a different way.

To write your own insertion operator, the function has to have the general form:

ostream & operator<<( ostream &, const type & );

where type is your data type.  If your data type is a structure (which it usually is), you should pass it by reference -- using the & -- because passing by value is very expensive (lots of data must be copied to the stack and cleaned up later).  Using the const keyword -- making it a constant reference -- guarantees that the operator will not attempt to change the contents of the data.

Inlining

Since each operator is a function, a C++ program tends to run slower than a comparable C program.  A programmer can use the inline keyword to attempt to speed things up.  By declaring a function inline, you are asking the compiler to essentially "cut-and-paste" the function's code directly in-place at the function call.

Function calls are very expensive in terms of CPU time, due to overhead from copying arguments and a return address to the stack, saving state information, and then cleaning up the stack.  By inlining code, your program will run quicker, but the resulting executable will be larger.

Furthermore, using inline is only a request -- the compiler doesn't have to inline your code if it decides that the code is too large or too complicated.  The inline keyword should only be used for functions that consist of 10 lines of code or less.

Inlining also makes sense in terms of software engineering.  Since the compiler does the cut-and-paste for you, your code is still written in one place only.  If you have to change the function's code, you don't have to go looking for calls throughout your program.

Listing 3.  Inlining An Insertion Operator

#include <iostream.h>

struct point
{
     int x;
     int y;
};

inline ostream & operator<<(ostream &, const point &);

int
main()
{
     point p;
     p.x = 1;
     p.y = 2;
     cout << "Point is " << p << endl;
     return 0;
}

ostream & 
operator<<(ostream & theStream, const point & thePoint)
{
     theStream << '(' << thePoint.x << ',' << thePoint.y << ')';
     return theStream;
}

Download this program

By inlining the above function, here's what the compiler sees after processing the inline request:

Listing 4.  Effect of Inlining an Insertion Operator

#include <iostream.h>

struct point
{
     int x;
     int y;
};

int
main()
{
     point p;
     p.x = 1;
     p.y = 2;
     cout << "Point is " << '(' << p.x << ',' << p.y << ')' << endl;
     return 0;
}

Download this program

Note that theStream has been replaced with cout and the function code cut-and-pasted where the call had previously been.


Back to the COMP435 page