C++ Annotations Version 4.0.0

Chapter 6: More About Operator Overloading

We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Mail to Frank Brokken or use an e-mail form. Please state the concerned document version, found in the title. If you're interested in a printable PostScript copy, use the form, or better yet, pick up your own copy by ftp from ftp.icce.rug.nl/pub/http.

Now that we've covered the overloaded assignment operator in depth, and now that we've seen some examples of other overloaded operators as well (i.e., the insertion and extraction operators), let's take a look at some other interesting examples of operator overloading.

6.1: Overloading operator[]()

As our next example of operator overloading, we present a class which is meant to operate an array of ints. Indexing the array elements occurs with the standard array operator [], but additionally the class checks for boundary overflow. Furthermore, the array operator is interesting in that it both produces a value and accepts a value, when used, respectively, as a right-hand value and a left-hand value in expressions.

An example of the use of the class is given here:

int main() { IntArray x(20); // 20 ints for (int i = 0; i < 20; i++) x[i] = i * 2; // assign the elements // produces boundary // overflow for (int i = 0; i <= 20; i++) cout << "At index " << i << ": value is " << x[i] << endl; return (0); } This example shows how an array is created to contain 20 ints. The elements of the array can be assigned or retrieved. The above example should produce a run-time error, generated by the class IntArray: the last for loop causing a boundary overflow, since x[20] is addressed while legal indices range from 0 to 19, inclusive.

We give the following class interface:

class IntArray { public: IntArray(int size = 1); // default size: 1 int IntArray(IntArray const &other); ~IntArray(); IntArray const &operator=(IntArray const &other); int &operator[](int index); // overloaded operator private: void destroy(); // standard functions // used to copy/destroy void copy(IntArray const &other); int *data, size; }; Concerning this class interface we remark:

The member functions of the class are presented next.

IntArray::IntArray(int sz) { if (sz < 1) { cout << "IntArray: size of array must be >= 1, not " << sz << "!" << endl; exit(1); } // remember size, create array size = sz; data = new int [sz]; } // copy constructor IntArray::IntArray(IntArray const &other) { copy(other); } // destructor IntArray::~IntArray() { delete [] data; } // overloaded assignment IntArray const &IntArray::operator=(IntArray const &other) { // take action only when no auto-assignment if (this != &other) { delete [] data; copy(other); } return (*this); } // copy() primitive IntArray::copy(IntArray const &other) { // set size size = other.size; // create array data = new int [size]; // copy other's values for (register int i = 0; i < size; i++) data[i] = other.data[i]; } // here is the overloaded array operator int &IntArray::operator[](int index) { // check for array boundary over/underflow if (index < 0 || index >= size) { cout << "IntArray: boundary overflow or underflow, index = " << index << ", should range from 0 to " << size - 1 << endl; exit(1); } return (data[index]); // emit the reference }

6.2: Overloading operator new(size_t)

If the operator new is overloaded, it must have a void * return type, and at least an argument of type size_t. The size_t type is defined in stddef.h), which must therefore be included when the operator new is overloaded.

It is also possible to define multiple versions of the operator new, as long as each version has its own unique set of arguments. The global new operator can still be used, through the ::-operator. If a class X overloads the operator new, then the system-provided operator new is activated by

X *x = ::new X();

Furthermore, the new [] construction will always use the default operator new.

An example of the overloaded operator new for the class X is the following:

#include <stddef.h> void *X::operator new(size_t sizeofX) { void *p = new char[sizeofX]; return (memset(p, 0, sizeof(X))); }

Now, what happens when the operator new is defined for the class X, assuming that class is defined as follows (For the sake of simplicity we have violated the principle of encapsulation here. The principle of encapsulation, however, is immaterial to the discussion of the workings of the operator new.):

class X { public: void *operator new(size_t sizeofX); int x, y, z; };

Next, consider the following program fragment:

#include "X.h" // class X interface etc. int main() { X *x = new X(); cout << x.x << ", " << x.y << ", "<< x.z << endl; return (0); }

This small program produces the following output:

0, 0, 0

Our little program performed the following actions:

Due to the initialization of the block of memory by the new operator the allocated X object was already initialized to zeros when the constructor was called.

Non-static object member functions are passed a (hidden) pointer to the object on which they should operate. This hidden pointer becomes the this pointer inside the memberfunction. This procedure is also followed by the constructor. In the following fragments of pseudo C++ the pointer is made visible. In the first part an X object is declared directly, in the second part of the example the (overloaded) operator new is used:

X::X(&x); // x's address is passed to the constructor // the compiler made 'x' available void // ask new to allocate the memory for an X *ptr = X::operator new(); X::X(ptr); // and let the constructor operate on the // memory returned by 'operator new' Notice that in the pseudo C++ fragment the member functions were treated as static functions of the class X. Actually, the operator new() operators is a static functions of its class: it cannot reach data members of its object, since it's normally the task of the operator new() to create room for that object first. It can do that by allocating enough memory, and by initializing the area as required. Next, the memory is passed over to the constructor (as the this pointer) for further processing. The fact that an overloaded operator new is in fact a static function, not requiring an object of its class can be illustrated in the following (discouraged in normal situations !) program fragment, which can be compiled without problems (assume class X has been defined and is available as before): int main() { X x; X::operator new(sizeof x); } The call to X::operator new() returns a void * to an initialized block of memory, the size of an X object.

The operator new can have multiple parameters. The first parameter again is the size_t parameter, other parameters must be passed during the call to the operator new. For example:

class X { public: void *operator new(size_t p1, unsigned p2); void *operator new(size_t p1, char const *fmt, ...); }; int main() { X *object1 = new(12) X(), *object2 = new("%d %d", 12, 13) X(), *object3 = new("%d %d", 12) X(); } The object (object1) is a pointer to an X object for which the memory has been allocated by the call to the first overloaded operator new, followed by the call of the constructor X() for that block of memory. The object (object2) is a pointer to an X object for which the memory has been allocated by the call to the second overloaded operator new, followed again by a call of the constructor X() for its block of memory. Notice that object3 also uses the second overloaded operator new(): that overloaded operator accepts a variable number of arguments, the first of which is a char const *.

6.3: Overloading operator delete(void *)

The delete operator may be overloaded too. The operator delete must have a void * argument, and an optional second argument of type size_t, which is the size in bytes of objects of the class for which the operator delete is overloaded. The returntype of the overloaded operator delete is void.

Therefore, in a class the operator delete may be overloaded using the following prototype:

void operator delete(void *);

or

void operator delete(void *, size_t);

The `home-made' delete operator is called after executing the class' destructor. So, the statement

delete ptr;

with ptr being a pointer to an object of the class X for which the operator delete was overloaded, boils down to the following statements:

X::~X(ptr); // call the destructor function itself // and do things with the memory pointed // to by ptr itself. Screen::operator delete(ptr, sizeof(*ptr));

The overloaded operator delete may do whatever it wants to do with the memory pointed to by ptr. It could, e.g., simply delete it. If that would be the preferred thing to do, then the default delete operator can be activated using the :: scope resolution operator. For example:

void X::operator delete(void *ptr) { // ... whatever else is considered necessary // use the default operator delete ::delete ptr; }

6.4: Cin, cout, cerr and their operators

This section describes how a class can be adapted in such a way that it can be used with the C++ streams cout and cerr and the insertion operator <<. Adaptation of a class for the usage with cin and its extraction operator >> occurs in a similar way and is not illustrated here.

The implementation of an overloaded operator << in the context of cout or cerr involves the base class of cout or cerr, which is ostream. This class is declared in the header file iostream.h and defines only overloaded operator functions for `basic' types, such as, int, char*, etc.. The purpose of this section is to show how an operator function can be defined which processes a new class, say Person (see section 5) , so that constructions as the following one become possible:

Person kr("Kernighan and Ritchie", "unknown", "unknown"); cout << "Name, address and phone number of Person kr:\n" << kr << '\n';

The statement cout << kr involves the operator <&lt and its two operands: an ostream& and a Person&. The proposed action is defined in a class-less operator function operator<<() expecting two arguments:

// declaration in, say, person.h ostream &operator<<(ostream &, Person const &); // definition in some source file ostream &operator<<(ostream &stream, Person const &pers) { return ( stream << "Name: " << pers.getname() << "Address: " << pers.getaddress() << "Phone: " << pers.getphone() ); }

Concerning this function we remark the following:

6.5: Conversion operators

A class may be constructed around a basic type. E.g., it is often fruitful to define a class String around the char *. Such a class may define all kinds of operations, like assignments. Take a look at the following class interface: class String { public: String(); String(char const *arg); ~String(); String(String const &other); String const &operator=(String const &rvalue); String const &operator=(char const *rvalue); private: char *string; }; Objects from this class can be initialized from a char const *, and also from a String itself. There is an overloaded assignment operator, allowing the assignment from a String object and from a char const * (Note that the assingment from a char const * also includes the null-pointer. An assignment like stringObject = 0 is perfectly in order.).

Usually, in classes that are less directly linked to their data than this String class, there will be an accessor member function, like char const *String::getstr() const. However, in the current context that looks a bit awkward, but it also doesn't seem to be the right way to go when an array of strings is defined, e.g., in a class StringArray, in which the operator[] is implemented to allow the access of individual strings. Take a look at the following class interface:

class StringArray { public: StringArray(unsigned size); StringArray(StringArray const &other); StringArray const &operator=(StringArray const &rvalue); ~StringArray(); String &operator[](unsigned index); private: String *store; unsigned n; };

The StringArray class has one interesting memberfunction: the overloaded array operator operator[]. It returns a String reference.

Using this operator assignments between the String elements can be realized:

StringArray sa(10); ... // assume the array is filled here sa[4] = sa[3]; // String to String assignment

It is also possible to assign a char const * to an element of sa:

sa[3] = "hello world";
When this is evaluated, the following steps are followed:

Now we try to do it the other way around: how to access the char const * that's stored in sa[3]? We try the following code:

char const *cp; cp = sa[3]; Well, this won't work: we would need an overloaded assignment operator for the 'class char const *'. However, there isn't such a class, and therefore we can't build that overloaded assignment operator (see also section
6.5). Furthermore, casting won't work: the compiler doesn't know how to cast a String to a char const *. How to proceed?

The naive solution is to resort to the accessor member function getstr():

cp = sa[3].getstr();
That solution would work, but it looks so clumsy.... A far better approach would be to a conversion operator.

A conversion operator is a kind of overloaded operator, but this time the overloading is used to cast the object to another type. Using a conversion operator a String object may be interpreted as a char const *, which can then be assigned to another char const *. Conversion operators can be implemented for all types for which a conversion is needed.

In the current example, the class String would need a conversion operator for a char const *. The general form of a conversion operator in the class interface is:

operator <type>();
With our String class, it would therefore be:
operator char const *();

The implementation of the conversion operator is straightforward:

String::operator char const *() { return (string); }

Notes:

For completion, the final String class interface, containing the conversion operator, looks like this:

class String { public: String(); String(char const *arg); ~String(); String(String const &other); String const &operator=(String const &rvalue); String const &operator=(char const *rvalue); operator char const *(); private: char *string; };

6.6: Overloadable Operators

The following operators can be overloaded: + - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new delete

However, some of these operators may only be overloaded as member functions within a class. This holds true for the '=', the '[]', the '()' and the '->' operators. Consequently, it isn't possible to redefine, e.g., the assignment operator globally in such a way that it accepts a char const * as an lvalue and a String & as an rvalue. Fortunately, that isn't necessary, as we have seen in section 6.4.